feat: 改进事件系统与全局快捷键 (#62)

This commit is contained in:
hstyi
2025-01-15 14:54:39 +08:00
committed by GitHub
parent a71493e52c
commit 45ea822fd6
137 changed files with 2860 additions and 1032 deletions

View File

@@ -42,14 +42,14 @@ dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
testImplementation(libs.hutool) testImplementation(libs.hutool)
testImplementation(libs.sshj) testImplementation(libs.sshj)
testImplementation(platform(libs.koin.bom))
testImplementation(libs.koin.core)
testImplementation(libs.jsch) testImplementation(libs.jsch)
testImplementation(libs.rhino) testImplementation(libs.rhino)
testImplementation(libs.delight.rhino.sandbox) testImplementation(libs.delight.rhino.sandbox)
testImplementation(platform(libs.testcontainers.bom)) testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers) testImplementation(libs.testcontainers)
// implementation(platform(libs.koin.bom))
// implementation(libs.koin.core)
implementation(libs.slf4j.api) implementation(libs.slf4j.api)
implementation(libs.pty4j) implementation(libs.pty4j)
implementation(libs.slf4j.tinylog) implementation(libs.slf4j.tinylog)
@@ -109,6 +109,12 @@ dependencies {
application { application {
val args = mutableListOf( val args = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED", "--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-XX:SoftMaxHeapSize=64m"
) )
if (os.isMacOsX) { if (os.isMacOsX) {
@@ -215,6 +221,11 @@ tasks.register<Exec>("jpackage") {
val options = mutableListOf( val options = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED", "--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g", "-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-XX:SoftMaxHeapSize=64m",
"-XX:+HeapDumpOnOutOfMemoryError", "-XX:+HeapDumpOnOutOfMemoryError",
"-Dlogger.console.level=off", "-Dlogger.console.level=off",
"-Dkotlinx.coroutines.debug=off", "-Dkotlinx.coroutines.debug=off",

View File

@@ -1,3 +1,4 @@
org.gradle.caching=true org.gradle.caching=true
org.gradle.parallel=true org.gradle.parallel=true
kotlin.code.style=official kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx4g

View File

@@ -2,21 +2,12 @@ package app.termora
object Actions { object Actions {
/**
* 打开设置
*/
const val SETTING = "SettingAction"
/** /**
* 将命令发送到多个会话 * 将命令发送到多个会话
*/ */
const val MULTIPLE = "MultipleAction" const val MULTIPLE = "MultipleAction"
/**
* 查找
*/
const val FIND_EVERYWHERE = "FindEverywhereAction"
/** /**
* 关键词高亮 * 关键词高亮
*/ */
@@ -38,15 +29,6 @@ object Actions {
*/ */
const val MACRO = "MacroAction" const val MACRO = "MacroAction"
/**
* 添加主机对话框
*/
const val ADD_HOST = "AddHostAction"
/**
* 打开一个主机
*/
const val OPEN_HOST = "OpenHostAction"
/** /**
* 终端日志记录 * 终端日志记录
@@ -57,4 +39,5 @@ object Actions {
* 打开 SFTP Tab Action * 打开 SFTP Tab Action
*/ */
const val SFTP = "SFTPAction" const val SFTP = "SFTPAction"
} }

View File

@@ -1,16 +0,0 @@
package app.termora
import org.jdesktop.swingx.action.BoundAction
import javax.swing.Icon
abstract class AnAction : BoundAction {
constructor() : super()
constructor(icon: Icon) : super() {
super.putValue(SMALL_ICON, icon)
}
constructor(name: String?) : super(name)
constructor(name: String?, icon: Icon?) : super(name, icon)
}

View File

@@ -16,14 +16,11 @@ import java.awt.Desktop
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.time.Duration import java.time.Duration
import java.util.*
import kotlin.math.ln import kotlin.math.ln
import kotlin.math.pow import kotlin.math.pow
import kotlin.reflect.KClass
object Application { object Application {
private val services = Collections.synchronizedMap(mutableMapOf<KClass<*>, Any>())
private lateinit var baseDataDir: File private lateinit var baseDataDir: File
@@ -125,22 +122,6 @@ object Application {
} }
} }
@Suppress("UNCHECKED_CAST")
fun <T : Any> getService(clazz: KClass<T>): T {
if (services.containsKey(clazz)) {
return services[clazz] as T
}
throw IllegalStateException("$clazz does not exist")
}
@Synchronized
fun registerService(clazz: KClass<*>, service: Any) {
if (services.containsKey(clazz)) {
throw IllegalStateException("$clazz already registered")
}
services[clazz] = service
}
private fun tryBrowse(uri: URI) { private fun tryBrowse(uri: URI) {
if (SystemInfo.isWindows) { if (SystemInfo.isWindows) {
ProcessBuilder("explorer", uri.toString()).start() ProcessBuilder("explorer", uri.toString()).start()

View File

@@ -1,10 +0,0 @@
package app.termora
/**
* 将在 JVM 进程退出时释放
*/
class ApplicationDisposable : Disposable {
companion object {
val instance by lazy { ApplicationDisposable() }
}
}

View File

@@ -1,6 +1,7 @@
package app.termora package app.termora
import app.termora.db.Database import app.termora.actions.ActionManager
import app.termora.keymap.KeymapManager
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatSystemProperties import com.formdev.flatlaf.FlatSystemProperties
import com.formdev.flatlaf.extras.FlatInspector import com.formdev.flatlaf.extras.FlatInspector
@@ -28,8 +29,8 @@ import java.io.RandomAccessFile
import java.nio.channels.FileLock import java.nio.channels.FileLock
import java.util.* import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
class ApplicationRunner { class ApplicationRunner {
private lateinit var singletonLock: FileLock private lateinit var singletonLock: FileLock
@@ -41,39 +42,62 @@ class ApplicationRunner {
} }
fun run() { fun run() {
measureTimeMillis {
// 覆盖 tinylog 配置 // 覆盖 tinylog 配置
setupTinylog() val setupTinylog = measureTimeMillis { setupTinylog() }
// 是否单例 // 是否单例
checkSingleton() val checkSingleton = measureTimeMillis { checkSingleton() }
// 打印系统信息 // 打印系统信息
printSystemInfo() val printSystemInfo = measureTimeMillis { printSystemInfo() }
SwingUtilities.invokeAndWait {
// 打开数据库 // 打开数据库
openDatabase() val openDatabase = measureTimeMillis { openDatabase() }
// 加载设置 // 加载设置
loadSettings() val loadSettings = measureTimeMillis { loadSettings() }
// 统计 // 统计
enableAnalytics() val enableAnalytics = measureTimeMillis { enableAnalytics() }
// init ActionManager、KeymapManager
@Suppress("OPT_IN_USAGE")
GlobalScope.launch(Dispatchers.IO) {
ActionManager.getInstance()
KeymapManager.getInstance()
}
// 设置 LAF // 设置 LAF
setupLaf() val setupLaf = measureTimeMillis { setupLaf() }
// 解密数据 // 解密数据
openDoor() val openDoor = measureTimeMillis { openDoor() }
// 启动主窗口 // 启动主窗口
startMainFrame() val startMainFrame = measureTimeMillis { startMainFrame() }
if (log.isDebugEnabled) {
log.debug("setupTinylog: {}ms", setupTinylog)
log.debug("checkSingleton: {}ms", checkSingleton)
log.debug("printSystemInfo: {}ms", printSystemInfo)
log.debug("openDatabase: {}ms", openDatabase)
log.debug("loadSettings: {}ms", loadSettings)
log.debug("enableAnalytics: {}ms", enableAnalytics)
log.debug("setupLaf: {}ms", setupLaf)
log.debug("openDoor: {}ms", openDoor)
log.debug("startMainFrame: {}ms", startMainFrame)
}
}.let {
if (log.isDebugEnabled) {
log.debug("run: {}ms", it)
}
} }
} }
private fun openDoor() { private fun openDoor() {
if (Doorman.instance.isWorking()) { if (Doorman.getInstance().isWorking()) {
if (!DoormanDialog(null).open()) { if (!DoormanDialog(null).open()) {
exitProcess(1) exitProcess(1)
} }
@@ -81,17 +105,11 @@ class ApplicationRunner {
} }
private fun startMainFrame() { private fun startMainFrame() {
val frame = TermoraFrame() TermoraFrameManager.getInstance().createWindow().isVisible = true
frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
frame.isVisible = true
} }
private fun loadSettings() { private fun loadSettings() {
val language = Database.instance.appearance.language val language = Database.getDatabase().appearance.language
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() } val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
if (log.isInfoEnabled) { if (log.isInfoEnabled) {
log.info("Language: {} , Locale: {}", language, locale) log.info("Language: {} , Locale: {}", language, locale)
@@ -110,10 +128,9 @@ class ApplicationRunner {
JDialog.setDefaultLookAndFeelDecorated(true) JDialog.setDefaultLookAndFeelDecorated(true)
} }
val themeManager = ThemeManager.instance val themeManager = ThemeManager.getInstance()
val settings = Database.instance val settings = Database.getDatabase()
var theme = settings.appearance.theme var theme = settings.appearance.theme
// 如果是跟随系统或者不存在样式,那么使用默认的 // 如果是跟随系统或者不存在样式,那么使用默认的
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) { if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) {
theme = if (OsThemeDetector.getDetector().isDark) { theme = if (OsThemeDetector.getDetector().isDark) {
@@ -125,6 +142,7 @@ class ApplicationRunner {
themeManager.change(theme, true) themeManager.change(theme, true)
if (Application.isUnknownVersion())
FlatInspector.install("ctrl shift alt X"); FlatInspector.install("ctrl shift alt X");
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true) UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
@@ -173,21 +191,21 @@ class ApplicationRunner {
} }
private fun printSystemInfo() { private fun printSystemInfo() {
if (log.isInfoEnabled) { if (log.isDebugEnabled) {
log.info("Welcome to ${Application.getName()} ${Application.getVersion()}!") log.debug("Welcome to ${Application.getName()} ${Application.getVersion()}!")
log.info( log.debug(
"JVM name: {} , vendor: {} , version: {}", "JVM name: {} , vendor: {} , version: {}",
SystemUtils.JAVA_VM_NAME, SystemUtils.JAVA_VM_NAME,
SystemUtils.JAVA_VM_VENDOR, SystemUtils.JAVA_VM_VENDOR,
SystemUtils.JAVA_VM_VERSION, SystemUtils.JAVA_VM_VERSION,
) )
log.info( log.debug(
"OS name: {} , version: {} , arch: {}", "OS name: {} , version: {} , arch: {}",
SystemUtils.OS_NAME, SystemUtils.OS_NAME,
SystemUtils.OS_VERSION, SystemUtils.OS_VERSION,
SystemUtils.OS_ARCH SystemUtils.OS_ARCH
) )
log.info("Base config dir: ${Application.getBaseDataDir().absolutePath}") log.debug("Base config dir: ${Application.getBaseDataDir().absolutePath}")
} }
} }
@@ -245,9 +263,8 @@ class ApplicationRunner {
private fun openDatabase() { private fun openDatabase() {
val dir = Application.getDatabaseFile()
try { try {
Database.open(dir) Database.getDatabase()
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) log.error(e.message, e)
@@ -296,10 +313,10 @@ class ApplicationRunner {
} }
private fun getAnalyticsUserID(): String { private fun getAnalyticsUserID(): String {
var id = Database.instance.properties.getString("AnalyticsUserID") var id = Database.getDatabase().properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) { if (id.isNullOrBlank()) {
id = UUID.randomUUID().toSimpleString() id = UUID.randomUUID().toSimpleString()
Database.instance.properties.putString("AnalyticsUserID", id) Database.getDatabase().properties.putString("AnalyticsUserID", id)
} }
return id return id
} }

View File

@@ -1,7 +1,7 @@
package app.termora package app.termora
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.db.Database
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@@ -358,7 +358,8 @@ class CustomizeToolBarDialog(
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false)) actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
} }
Database.instance.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions)) Database.getDatabase()
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
super.doOKAction() super.doOKAction()
} }

View File

@@ -1,8 +1,9 @@
package app.termora.db package app.termora
import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlight
import app.termora.keymap.KeyShortcut
import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.sync.SyncType import app.termora.sync.SyncType
@@ -13,10 +14,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.util.* import java.util.*
import javax.swing.KeyStroke
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
@@ -26,24 +29,15 @@ import kotlin.time.Duration.Companion.minutes
class Database private constructor(private val env: Environment) : Disposable { class Database private constructor(private val env: Environment) : Disposable {
companion object { companion object {
private const val KEYMAP_STORE = "Keymap"
private const val HOST_STORE = "Host" private const val HOST_STORE = "Host"
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight" private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
private const val MACRO_STORE = "Macro" private const val MACRO_STORE = "Macro"
private const val KEY_PAIR_STORE = "KeyPair" private const val KEY_PAIR_STORE = "KeyPair"
private val log = LoggerFactory.getLogger(Database::class.java) private val log = LoggerFactory.getLogger(Database::class.java)
private lateinit var database: Database
val instance by lazy {
if (!::database.isInitialized) {
throw UnsupportedOperationException("Database has not been initialized!")
}
database
}
fun open(dir: File) { private fun open(dir: File): Database {
if (::database.isInitialized) {
throw UnsupportedOperationException("Database is already open")
}
val config = EnvironmentConfig() val config = EnvironmentConfig()
// 32MB // 32MB
config.setLogFileSize(1024 * 32) config.setLogFileSize(1024 * 32)
@@ -51,8 +45,12 @@ class Database private constructor(private val env: Environment) : Disposable {
// 5m // 5m
config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt()) config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt())
val environment = Environments.newInstance(dir, config) val environment = Environments.newInstance(dir, config)
database = Database(environment) return Database(environment)
Disposer.register(ApplicationDisposable.instance, database) }
fun getDatabase(): Database {
return ApplicationScope.forApplicationScope()
.getOrCreate(Database::class) { open(Application.getDatabaseFile()) }
} }
} }
@@ -62,7 +60,60 @@ class Database private constructor(private val env: Environment) : Disposable {
val appearance by lazy { Appearance() } val appearance by lazy { Appearance() }
val sync by lazy { Sync() } val sync by lazy { Sync() }
private val doorman get() = Doorman.instance private val doorman get() = Doorman.getInstance()
fun getKeymaps(): Collection<Keymap> {
val array = env.computeInTransaction { tx ->
openCursor<JsonObject>(tx, KEYMAP_STORE) { _, value ->
ohMyJson.decodeFromString<JsonObject>(value)
}.values
}
val shortcuts = mutableListOf<Keymap>()
for (json in array.iterator()) {
val name = json["name"]?.jsonPrimitive?.content ?: continue
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: false
val keymap = Keymap(name, null, readonly)
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
val keyboard = shortcut["keyboard"]?.jsonPrimitive?.booleanOrNull ?: true
val actionIds = ohMyJson.decodeFromJsonElement<List<String>>(
shortcut["actionIds"]?.jsonArray
?: continue
)
if (keyboard) {
val keyShortcut = KeyShortcut(KeyStroke.getKeyStroke(keyStroke))
for (actionId in actionIds) {
keymap.addShortcut(actionId, keyShortcut)
}
}
}
shortcuts.add(keymap)
}
return shortcuts
}
fun addKeymap(keymap: Keymap) {
env.executeInTransaction {
put(it, KEYMAP_STORE, keymap.name, keymap.toJSON())
if (log.isDebugEnabled) {
log.debug("Added Keymap: ${keymap.name}")
}
}
}
fun removeKeymap(name: String) {
env.executeInTransaction {
delete(it, KEYMAP_STORE, name)
if (log.isDebugEnabled) {
log.debug("Removed Keymap: $name")
}
}
}
fun getHosts(): Collection<Host> { fun getHosts(): Collection<Host> {
@@ -413,7 +464,7 @@ class Database private constructor(private val env: Environment) : Disposable {
/** /**
* 字体大小 * 字体大小
*/ */
var fontSize by IntPropertyDelegate(16) var fontSize by IntPropertyDelegate(14)
/** /**
* 最大行数 * 最大行数
@@ -459,7 +510,7 @@ class Database private constructor(private val env: Environment) : Disposable {
* 安全的通用属性 * 安全的通用属性
*/ */
open inner class SafetyProperties(name: String) : Property(name) { open inner class SafetyProperties(name: String) : Property(name) {
private val doorman get() = Doorman.instance private val doorman get() = Doorman.getInstance()
public override fun getString(key: String): String? { public override fun getString(key: String): String? {
var value = super.getString(key) var value = super.getString(key)

View File

@@ -1,5 +1,7 @@
package app.termora package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
@@ -7,7 +9,6 @@ import com.jetbrains.JBR
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
@@ -21,6 +22,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
companion object { companion object {
const val DEFAULT_ACTION = "DEFAULT_ACTION" const val DEFAULT_ACTION = "DEFAULT_ACTION"
private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
} }
@@ -38,9 +40,21 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
protected var lostFocusDispose = false protected var lostFocusDispose = false
protected var escapeDispose = true protected var escapeDispose = true
var processGlobalKeymap: Boolean
get() {
val v = super.rootPane.getClientProperty(PROCESS_GLOBAL_KEYMAP)
if (v is Boolean) {
return v
}
return false
}
protected set(value) {
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
}
protected fun init() { protected fun init() {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
initTitleBar() initTitleBar()
@@ -132,7 +146,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close") inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
rootPane.actionMap.put("close", object : AnAction() { rootPane.actionMap.put("close", object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
doCancelAction() doCancelAction()
} }
}) })
@@ -154,12 +168,12 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
if (SystemInfo.isWindows) { if (SystemInfo.isWindows) {
addWindowListener(object : WindowAdapter(), ThemeChangeListener { addWindowListener(object : WindowAdapter(), ThemeChangeListener {
override fun windowClosed(e: WindowEvent) { override fun windowClosed(e: WindowEvent) {
ThemeManager.instance.removeThemeChangeListener(this) ThemeManager.getInstance().removeThemeChangeListener(this)
} }
override fun windowOpened(e: WindowEvent) { override fun windowOpened(e: WindowEvent) {
onChanged() onChanged()
ThemeManager.instance.addThemeChangeListener(this) ThemeManager.getInstance().addThemeChangeListener(this)
} }
override fun onChanged() { override fun onChanged() {
@@ -190,7 +204,8 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
putValue(DEFAULT_ACTION, true) putValue(DEFAULT_ACTION, true)
} }
override fun actionPerformed(e: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
doOKAction() doOKAction()
} }
@@ -198,7 +213,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) { protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
doCancelAction() doCancelAction()
} }

View File

@@ -2,16 +2,17 @@ package app.termora
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.AES.encodeBase64String import app.termora.AES.encodeBase64String
import app.termora.db.Database
class PasswordWrongException : RuntimeException() class PasswordWrongException : RuntimeException()
class Doorman private constructor() { class Doorman private constructor() : Disposable {
private val properties get() = Database.instance.properties private val properties get() = Database.getDatabase().properties
private var key = byteArrayOf() private var key = byteArrayOf()
companion object { companion object {
val instance by lazy { Doorman() } fun getInstance(): Doorman {
return ApplicationScope.forApplicationScope().getOrCreate(Doorman::class) { Doorman() }
}
} }
fun isWorking(): Boolean { fun isWorking(): Boolean {
@@ -82,4 +83,8 @@ class Doorman private constructor() {
checkIsWorking() checkIsWorking()
return key.contentEquals(convertKey(password)) return key.contentEquals(convertKey(password))
} }
override fun dispose() {
key = byteArrayOf()
}
} }

View File

@@ -1,7 +1,8 @@
package app.termora package app.termora
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.db.Database import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.terminal.ControlCharacters import app.termora.terminal.ControlCharacters
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
@@ -17,7 +18,6 @@ import org.slf4j.LoggerFactory
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
import java.awt.event.ActionEvent
import java.awt.event.KeyAdapter import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.imageio.ImageIO import javax.imageio.ImageIO
@@ -95,7 +95,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
.add(safeBtn).xy(4, rows).apply { rows += step } .add(safeBtn).xy(4, rows).apply { rows += step }
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step } .add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) { .add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val option = OptionPane.showConfirmDialog( val option = OptionPane.showConfirmDialog(
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"), this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
options = arrayOf( options = arrayOf(
@@ -130,10 +130,11 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
} }
try { try {
val keyBackup = Database.instance.properties.getString("doorman-key-backup") val keyBackup = Database.getDatabase()
.properties.getString("doorman-key-backup")
?: throw IllegalStateException("doorman-key-backup is null") ?: throw IllegalStateException("doorman-key-backup is null")
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64()) val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
Doorman.instance.work(key) Doorman.getInstance().work(key)
} catch (e: Exception) { } catch (e: Exception) {
OptionPane.showMessageDialog( OptionPane.showMessageDialog(
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"), this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
@@ -157,7 +158,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
} }
try { try {
Doorman.instance.work(passwordTextField.password) Doorman.getInstance().work(passwordTextField.password)
} catch (e: Exception) { } catch (e: Exception) {
if (e is PasswordWrongException) { if (e is PasswordWrongException) {
OptionPane.showMessageDialog( OptionPane.showMessageDialog(

View File

@@ -14,7 +14,7 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
generalOption.remarkTextArea.text = host.remark generalOption.remarkTextArea.text = host.remark
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
if (host.authentication.type == AuthenticationType.PublicKey) { if (host.authentication.type == AuthenticationType.PublicKey) {
val ohKeyPair = KeyManager.instance.getOhKeyPair(host.authentication.password) val ohKeyPair = KeyManager.getInstance().getOhKeyPair(host.authentication.password)
if (ohKeyPair != null) { if (ohKeyPair != null) {
generalOption.publicKeyTextField.text = ohKeyPair.name generalOption.publicKeyTextField.text = ohKeyPair.name
generalOption.publicKeyTextField.putClientProperty(OhKeyPair::class, ohKeyPair) generalOption.publicKeyTextField.putClientProperty(OhKeyPair::class, ohKeyPair)

View File

@@ -1,12 +1,13 @@
package app.termora package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.sshd.client.SshClient import org.apache.sshd.client.SshClient
import org.apache.sshd.client.session.ClientSession import org.apache.sshd.client.session.ClientSession
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.event.ActionEvent
import javax.swing.* import javax.swing.*
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) { class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
@@ -40,7 +41,7 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
private fun createTestConnectionAction(): AbstractAction { private fun createTestConnectionAction(): AbstractAction {
return object : AnAction(I18n.getString("termora.new-host.test-connection")) { return object : AnAction(I18n.getString("termora.new-host.test-connection")) {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (!pane.validateFields()) { if (!pane.validateFields()) {
return return
} }

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import java.util.* import java.util.*
interface HostListener : EventListener { interface HostListener : EventListener {
@@ -12,10 +11,12 @@ interface HostListener : EventListener {
class HostManager private constructor() { class HostManager private constructor() {
companion object { companion object {
val instance by lazy { HostManager() } fun getInstance(): HostManager {
return ApplicationScope.forApplicationScope().getOrCreate(HostManager::class) { HostManager() }
}
} }
private val database get() = Database.instance private val database get() = Database.getDatabase()
private val listeners = mutableListOf<HostListener>() private val listeners = mutableListOf<HostListener>()
fun addHost(host: Host, notify: Boolean = true) { fun addHost(host: Host, notify: Boolean = true) {

View File

@@ -9,8 +9,9 @@ import java.beans.PropertyChangeEvent
import javax.swing.Icon import javax.swing.Icon
abstract class HostTerminalTab( abstract class HostTerminalTab(
val windowScope: WindowScope,
val host: Host, val host: Host,
protected val terminal: Terminal = TerminalFactory.instance.createTerminal() protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
) : PropertyTerminalTab() { ) : PropertyTerminalTab() {
companion object { companion object {
val Host = DataKey(app.termora.Host::class) val Host = DataKey(app.termora.Host::class)

View File

@@ -1,6 +1,8 @@
package app.termora package app.termora
import app.termora.db.Database
import app.termora.actions.NewHostAction
import app.termora.actions.OpenHostAction
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.icons.FlatTreeClosedIcon import com.formdev.flatlaf.icons.FlatTreeClosedIcon
import com.formdev.flatlaf.icons.FlatTreeOpenIcon import com.formdev.flatlaf.icons.FlatTreeOpenIcon
@@ -24,7 +26,7 @@ import javax.swing.tree.TreeSelectionModel
class HostTree : JTree(), Disposable { class HostTree : JTree(), Disposable {
private val hostManager get() = HostManager.instance private val hostManager get() = HostManager.getInstance()
private val editor = OutlineTextField(64) private val editor = OutlineTextField(64)
var contextmenu = true var contextmenu = true
@@ -83,7 +85,7 @@ class HostTree : JTree(), Disposable {
}) })
val state = Database.instance.properties.getString("HostTreeExpansionState") val state = Database.getDatabase().properties.getString("HostTreeExpansionState")
if (state != null) { if (state != null) {
TreeUtils.loadExpansionState(this@HostTree, state) TreeUtils.loadExpansionState(this@HostTree, state)
} }
@@ -132,8 +134,8 @@ class HostTree : JTree(), Disposable {
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) { if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val host = lastSelectedPathComponent val host = lastSelectedPathComponent
if (host is Host && host.protocol != Protocol.Folder) { if (host is Host && host.protocol != Protocol.Folder) {
ActionManager.getInstance().getAction(Actions.OPEN_HOST) ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, host)) ?.actionPerformed(OpenHostActionEvent(e.source, host, e))
} }
} }
} }
@@ -328,13 +330,13 @@ class HostTree : JTree(), Disposable {
popupMenu.addSeparator() popupMenu.addSeparator()
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property")) val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
open.addActionListener { open.addActionListener { evt ->
getSelectionNodes() getSelectionNodes()
.filter { it.protocol != Protocol.Folder } .filter { it.protocol != Protocol.Folder }
.forEach { .forEach {
ActionManager.getInstance() ActionManager.getInstance()
.getAction(Actions.OPEN_HOST) .getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, it)) ?.actionPerformed(OpenHostActionEvent(evt.source, it, evt))
} }
} }
@@ -412,7 +414,8 @@ class HostTree : JTree(), Disposable {
newHost.addActionListener(object : AbstractAction() { newHost.addActionListener(object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
showAddHostDialog() ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
?.actionPerformed(e)
} }
}) })
@@ -451,30 +454,8 @@ class HostTree : JTree(), Disposable {
popupMenu.show(this, event.x, event.y) popupMenu.show(this, event.x, event.y)
} }
fun showAddHostDialog() {
var lastHost = lastSelectedPathComponent
if (lastHost !is Host) {
return
}
if (lastHost.protocol != Protocol.Folder) { fun expandNode(node: Host, including: Boolean = false) {
val p = model.getParent(lastHost) ?: return
lastHost = p
}
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this))
dialog.isVisible = true
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
runCatchingHost(host)
expandNode(lastHost)
selectionPath = TreePath(model.getPathToRoot(host))
}
private fun expandNode(node: Host, including: Boolean = false) {
expandPath(TreePath(model.getPathToRoot(node))) expandPath(TreePath(model.getPathToRoot(node)))
if (including) { if (including) {
model.getChildren(node).forEach { expandNode(it, true) } model.getChildren(node).forEach { expandNode(it, true) }
@@ -552,7 +533,7 @@ class HostTree : JTree(), Disposable {
} }
override fun dispose() { override fun dispose() {
Database.instance.properties.putString( Database.getDatabase().properties.putString(
"HostTreeExpansionState", "HostTreeExpansionState",
TreeUtils.saveExpansionState(this) TreeUtils.saveExpansionState(this)
) )

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
@@ -51,7 +50,7 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
addWindowListener(object : WindowAdapter() { addWindowListener(object : WindowAdapter() {
override fun windowActivated(e: WindowEvent) { override fun windowActivated(e: WindowEvent) {
removeWindowListener(this) removeWindowListener(this)
val state = Database.instance.properties.getString("HostTreeDialog.HostTreeExpansionState") val state = Database.getDatabase().properties.getString("HostTreeDialog.HostTreeExpansionState")
if (state != null) { if (state != null) {
TreeUtils.loadExpansionState(tree, state) TreeUtils.loadExpansionState(tree, state)
} }
@@ -71,7 +70,7 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
addWindowListener(object : WindowAdapter() { addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) { override fun windowClosed(e: WindowEvent) {
Database.instance.properties.putString( Database.getDatabase().properties.putString(
"HostTreeDialog.HostTreeExpansionState", "HostTreeDialog.HostTreeExpansionState",
TreeUtils.saveExpansionState(tree) TreeUtils.saveExpansionState(tree)
) )

View File

@@ -10,7 +10,7 @@ class HostTreeModel : TreeModel {
val listeners = mutableListOf<TreeModelListener>() val listeners = mutableListOf<TreeModelListener>()
private val hostManager get() = HostManager.instance private val hostManager get() = HostManager.getInstance()
private val hosts = mutableMapOf<String, Host>() private val hosts = mutableMapOf<String, Host>()
private val myRoot by lazy { private val myRoot by lazy {
Host( Host(

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.actions.AnAction
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter
import org.jdesktop.swingx.JXHyperlink import org.jdesktop.swingx.JXHyperlink

View File

@@ -40,12 +40,17 @@ object I18n {
} }
fun getString(key: String, vararg args: Any): String { fun getString(key: String, vararg args: Any): String {
try { val text = getString(key)
val text = substitutor.replace(bundle.getString(key))
if (args.isNotEmpty()) { if (args.isNotEmpty()) {
return MessageFormat.format(text, *args) return MessageFormat.format(text, *args)
} }
return text return text
}
fun getString(key: String): String {
try {
return substitutor.replace(bundle.getString(key))
} catch (e: MissingResourceException) { } catch (e: MissingResourceException) {
if (log.isWarnEnabled) { if (log.isWarnEnabled) {
log.warn(e.message, e) log.warn(e.message, e)
@@ -54,4 +59,5 @@ object I18n {
} }
} }
} }

View File

@@ -13,7 +13,10 @@ object Icons {
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") } val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") } val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") } val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") }
val fitContent by lazy { DynamicIcon("icons/fitContent.svg", "icons/fitContent_dark.svg") }
val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") } val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
val copy by lazy { DynamicIcon("icons/copy.svg", "icons/copy_dark.svg") }
val delete by lazy { DynamicIcon("icons/delete.svg", "icons/delete_dark.svg") }
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") } val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
val empty by lazy { DynamicIcon("icons/empty.svg") } val empty by lazy { DynamicIcon("icons/empty.svg") }
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") } val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }

View File

@@ -4,11 +4,11 @@ import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets import org.apache.commons.io.Charsets
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class LocalTerminalTab(host: Host) : PtyHostTerminalTab(host) { class LocalTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
override suspend fun openPtyConnector(): PtyConnector { override suspend fun openPtyConnector(): PtyConnector {
val winSize = terminalPanel.winSize() val winSize = terminalPanel.winSize()
val ptyConnector = PtyConnectorFactory.instance.createPtyConnector( val ptyConnector = PtyConnectorFactory.getInstance(windowScope).createPtyConnector(
winSize.rows, winSize.cols, winSize.rows, winSize.cols,
host.options.envs(), host.options.envs(),
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8), Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate import app.termora.terminal.PtyConnectorDelegate
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
@@ -7,10 +8,15 @@ import org.jdesktop.swingx.action.ActionManager
/** /**
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。 * 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
*/ */
class MultiplePtyConnector(private val myConnector: PtyConnector) : PtyConnectorDelegate(myConnector) { class MultiplePtyConnector(
private val myConnector: PtyConnector
) : PtyConnectorDelegate(myConnector) {
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE) private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
private val ptyConnectors get() = PtyConnectorFactory.instance.getPtyConnectors() private val ptyConnectors
get() = ApplicationScope.forApplicationScope()
.windowScopes().map { PtyConnectorFactory.getInstance(it).getPtyConnectors() }
.flatten()
override fun write(buffer: ByteArray, offset: Int, len: Int) { override fun write(buffer: ByteArray, offset: Int, len: Int) {
if (isMultiple) { if (isMultiple) {

View File

@@ -1,12 +1,13 @@
package app.termora package app.termora
import app.termora.actions.ActionManager
import app.termora.terminal.Terminal import app.termora.terminal.Terminal
import app.termora.terminal.TerminalColor import app.termora.terminal.TerminalColor
import app.termora.terminal.TextStyle import app.termora.terminal.TextStyle
import app.termora.terminal.panel.TerminalDisplay import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener import app.termora.terminal.panel.TerminalPaintListener
import app.termora.terminal.panel.TerminalPanel import app.termora.terminal.panel.TerminalPanel
import org.jdesktop.swingx.action.ActionManager
import java.awt.Color import java.awt.Color
import java.awt.Graphics import java.awt.Graphics

View File

@@ -6,6 +6,7 @@ class MyTabbedPane : FlatTabbedPane() {
override fun setSelectedIndex(index: Int) { override fun setSelectedIndex(index: Int) {
val oldIndex = selectedIndex val oldIndex = selectedIndex
super.setSelectedIndex(index) super.setSelectedIndex(index)
firePropertyChange("selectedIndex", oldIndex,index) firePropertyChange("selectedIndex", oldIndex, index)
} }
} }

View File

@@ -1,5 +1,7 @@
package app.termora package app.termora
import java.awt.event.ActionEvent import app.termora.actions.AnActionEvent
import java.util.*
class OpenHostActionEvent(source: Any, val host: Host) : ActionEvent(source, ACTION_PERFORMED, String()) class OpenHostActionEvent(source: Any, val host: Host, event: EventObject) :
AnActionEvent(source, String(), event)

View File

@@ -1,22 +1,26 @@
package app.termora package app.termora
import app.termora.db.Database
import app.termora.macro.MacroPtyConnector import app.termora.macro.MacroPtyConnector
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate import app.termora.terminal.PtyConnectorDelegate
import app.termora.terminal.PtyProcessConnector import app.termora.terminal.PtyProcessConnector
import com.pty4j.PtyProcessBuilder import com.pty4j.PtyProcessBuilder
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.slf4j.LoggerFactory
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
class PtyConnectorFactory { class PtyConnectorFactory : Disposable {
private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>()) private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>())
private val database get() = Database.instance private val database get() = Database.getDatabase()
companion object { companion object {
val instance by lazy { PtyConnectorFactory() } private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
fun getInstance(scope: Scope): PtyConnectorFactory {
return scope.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
}
} }
fun createPtyConnector( fun createPtyConnector(
@@ -29,12 +33,25 @@ class PtyConnectorFactory {
envs["TERM"] = "xterm-256color" envs["TERM"] = "xterm-256color"
envs.putAll(env) envs.putAll(env)
if (SystemUtils.IS_OS_UNIX) {
if (!envs.containsKey("LANG")) {
val locale = Locale.getDefault()
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
}
}
}
val command = database.terminal.localShell val command = database.terminal.localShell
val commands = mutableListOf(command) val commands = mutableListOf(command)
if (SystemUtils.IS_OS_UNIX) { if (SystemUtils.IS_OS_UNIX) {
commands.add("-l") commands.add("-l")
} }
if (log.isDebugEnabled) {
log.debug("command: {} , envs: {}", commands.joinToString(" "), envs)
}
val ptyProcess = PtyProcessBuilder(commands.toTypedArray()) val ptyProcess = PtyProcessBuilder(commands.toTypedArray())
.setEnvironment(envs) .setEnvironment(envs)
.setInitialRows(rows) .setInitialRows(rows)

View File

@@ -10,9 +10,10 @@ import javax.swing.JComponent
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
abstract class PtyHostTerminalTab( abstract class PtyHostTerminalTab(
windowScope: WindowScope,
host: Host, host: Host,
terminal: Terminal = TerminalFactory.instance.createTerminal() terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
) : HostTerminalTab(host, terminal) { ) : HostTerminalTab(windowScope, host, terminal) {
companion object { companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java) private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
@@ -22,8 +23,9 @@ abstract class PtyHostTerminalTab(
private var readerJob: Job? = null private var readerJob: Job? = null
private val ptyConnectorDelegate = PtyConnectorDelegate() private val ptyConnectorDelegate = PtyConnectorDelegate()
protected val terminalPanel = TerminalPanelFactory.instance.createTerminalPanel(terminal, ptyConnectorDelegate) protected val terminalPanel =
protected val ptyConnectorFactory get() = PtyConnectorFactory.instance TerminalPanelFactory.getInstance(windowScope).createTerminalPanel(terminal, ptyConnectorDelegate)
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
override fun start() { override fun start() {
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {

View File

@@ -28,7 +28,7 @@ import javax.swing.JComponent
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) { class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
companion object { companion object {
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java) private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
} }

View File

@@ -0,0 +1,173 @@
package app.termora
import org.slf4j.LoggerFactory
import java.awt.Component
import java.awt.Window
import java.util.concurrent.ConcurrentHashMap
import javax.swing.JPopupMenu
import javax.swing.SwingUtilities
import kotlin.reflect.KClass
@Suppress("UNCHECKED_CAST")
open class Scope(
private val beans: MutableMap<KClass<*>, Any> = ConcurrentHashMap(),
private val properties: MutableMap<String, Any> = ConcurrentHashMap()
) : Disposable {
fun <T : Any> get(clazz: KClass<T>): T {
return beans[clazz] as T
}
fun <T : Any> getOrCreate(clazz: KClass<T>, create: () -> T): T {
if (beans.containsKey(clazz)) {
return get(clazz)
}
synchronized(clazz) {
if (beans.containsKey(clazz)) {
return get(clazz)
}
val instance = create.invoke()
beans[clazz] = instance
if (instance is Disposable) {
Disposer.register(this, instance)
}
return instance
}
}
fun putBoolean(name: String, value: Boolean) {
properties[name] = value
}
fun getBoolean(name: String, defaultValue: Boolean): Boolean {
return properties[name]?.toString()?.toBoolean() ?: defaultValue
}
fun putAny(name: String, value: Any) {
properties[name] = value
}
fun getAny(name: String, defaultValue: Any): Any {
return properties[name]?.toString() ?: defaultValue
}
fun getAnyOrNull(name: String): Any? {
return properties[name]
}
override fun dispose() {
beans.clear()
}
}
class ApplicationScope private constructor() : Scope() {
private val scopes = mutableMapOf<Any, WindowScope>()
companion object {
private val log = LoggerFactory.getLogger(ApplicationScope::class.java)
private val instance by lazy { ApplicationScope() }
fun forApplicationScope(): ApplicationScope {
return instance
}
fun forWindowScope(frame: TermoraFrame): WindowScope {
return forApplicationScope().forWindowScope(frame)
}
fun forWindowScope(container: Component): WindowScope {
val frame = getFrameForComponent(container)
?: throw IllegalStateException("Unexpected owner in $container")
return forWindowScope(frame)
}
fun windowScopes(): List<WindowScope> {
return forApplicationScope().windowScopes()
}
private fun getFrameForComponent(component: Component): TermoraFrame? {
if (component is TermoraFrame) {
return component
}
var owner = SwingUtilities.getWindowAncestor(component) as Component?
if (owner is TermoraFrame) {
return owner
}
if (owner == null) {
owner = component
}
while (owner != null) {
if (owner is JPopupMenu) {
owner = owner.invoker
if (owner is TermoraFrame) {
return owner
}
continue
}
owner = owner.parent
if (owner is TermoraFrame) {
return owner
}
}
return null
}
}
private fun forWindowScope(frame: TermoraFrame): WindowScope {
val windowScope = scopes.getOrPut(frame) { WindowScope(frame) }
Disposer.register(windowScope, object : Disposable {
override fun dispose() {
scopes.remove(frame)
}
})
return windowScope
}
fun windowScopes(): List<WindowScope> {
return scopes.values.toList()
}
override fun dispose() {
if (log.isInfoEnabled) {
log.info("ApplicationScope disposed")
}
super.dispose()
}
}
class WindowScope(
val window: Window,
) : Scope() {
companion object {
private val log = LoggerFactory.getLogger(WindowScope::class.java)
}
override fun dispose() {
if (log.isInfoEnabled) {
log.info("WindowScope disposed")
}
super.dispose()
}
}

View File

@@ -48,10 +48,10 @@ class SearchableHostTreeModel(
val children = model.getChildren(parent) val children = model.getChildren(parent)
if (children.isEmpty()) return emptyList() if (children.isEmpty()) return emptyList()
return children.filter { e -> return children.filter { e ->
filter.invoke(e) && e.name.contains(text, true) || TreeUtils.children(model, e, true) filter.invoke(e)
.filterIsInstance<Host>().any { && e.name.contains(text, true)
it.name.contains(text, true) || e.host.contains(text, true)
} || TreeUtils.children(model, e, true).filterIsInstance<Host>().any { it.name.contains(text, true) || it.host.contains(text, true) }
} }
} }

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
@@ -13,7 +12,7 @@ import javax.swing.UIManager
class SettingsDialog(owner: Window) : DialogWrapper(owner) { class SettingsDialog(owner: Window) : DialogWrapper(owner) {
private val optionsPane = SettingsOptionsPane() private val optionsPane = SettingsOptionsPane()
private val properties get() = Database.instance.properties private val properties get() = Database.getDatabase().properties
init { init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")) size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))

View File

@@ -2,8 +2,10 @@ package app.termora
import app.termora.AES.encodeBase64String import app.termora.AES.encodeBase64String
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.db.Database import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.highlight.KeywordHighlightManager import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.KeymapPanel
import app.termora.keymgr.KeyManager import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager import app.termora.macro.MacroManager
import app.termora.native.FileChooser import app.termora.native.FileChooser
@@ -40,7 +42,6 @@ import org.slf4j.LoggerFactory
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component import java.awt.Component
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.awt.event.ActionEvent
import java.awt.event.ItemEvent import java.awt.event.ItemEvent
import java.io.File import java.io.File
import java.net.URI import java.net.URI
@@ -53,7 +54,7 @@ import kotlin.time.Duration.Companion.milliseconds
class SettingsOptionsPane : OptionsPane() { class SettingsOptionsPane : OptionsPane() {
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane) private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
private val database get() = Database.instance private val database get() = Database.getDatabase()
companion object { companion object {
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java) private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
@@ -96,6 +97,7 @@ class SettingsOptionsPane : OptionsPane() {
init { init {
addOption(AppearanceOption()) addOption(AppearanceOption())
addOption(TerminalOption()) addOption(TerminalOption())
addOption(KeyShortcutsOption())
addOption(CloudSyncOption()) addOption(CloudSyncOption())
addOption(DoormanOption()) addOption(DoormanOption())
addOption(AboutOption()) addOption(AboutOption())
@@ -103,7 +105,7 @@ class SettingsOptionsPane : OptionsPane() {
} }
private inner class AppearanceOption : JPanel(BorderLayout()), Option { private inner class AppearanceOption : JPanel(BorderLayout()), Option {
val themeManager = ThemeManager.instance val themeManager = ThemeManager.getInstance()
val themeComboBox = FlatComboBox<String>() val themeComboBox = FlatComboBox<String>()
val languageComboBox = FlatComboBox<String>() val languageComboBox = FlatComboBox<String>()
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
@@ -217,7 +219,7 @@ class SettingsOptionsPane : OptionsPane() {
.add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows) .add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows)
.add(languageComboBox).xy(3, rows) .add(languageComboBox).xy(3, rows)
.add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) { .add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) {
override fun actionPerformed(evt: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n")) Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
} }
})).xy(5, rows).apply { rows += step } })).xy(5, rows).apply { rows += step }
@@ -234,7 +236,7 @@ class SettingsOptionsPane : OptionsPane() {
private val shellComboBox = FlatComboBox<String>() private val shellComboBox = FlatComboBox<String>()
private val maxRowsTextField = IntSpinner(0, 0) private val maxRowsTextField = IntSpinner(0, 0)
private val fontSizeTextField = IntSpinner(0, 9, 99) private val fontSizeTextField = IntSpinner(0, 9, 99)
private val terminalSetting get() = Database.instance.terminal private val terminalSetting get() = Database.getDatabase().terminal
private val selectCopyComboBox = YesOrNoComboBox() private val selectCopyComboBox = YesOrNoComboBox()
init { init {
@@ -270,7 +272,7 @@ class SettingsOptionsPane : OptionsPane() {
if (it.stateChange == ItemEvent.SELECTED) { if (it.stateChange == ItemEvent.SELECTED) {
val style = cursorStyleComboBox.selectedItem as CursorStyle val style = cursorStyleComboBox.selectedItem as CursorStyle
terminalSetting.cursor = style terminalSetting.cursor = style
TerminalFactory.instance.getTerminals().forEach { e -> TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
e.getTerminalModel().setData(DataKey.CursorStyle, style) e.getTerminalModel().setData(DataKey.CursorStyle, style)
} }
} }
@@ -280,7 +282,7 @@ class SettingsOptionsPane : OptionsPane() {
debugComboBox.addItemListener { e -> debugComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) { if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.debug = debugComboBox.selectedItem as Boolean terminalSetting.debug = debugComboBox.selectedItem as Boolean
TerminalFactory.instance.getTerminals().forEach { TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug) it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
} }
} }
@@ -296,7 +298,10 @@ class SettingsOptionsPane : OptionsPane() {
} }
private fun fireFontChanged() { private fun fireFontChanged() {
TerminalPanelFactory.instance.fireResize() ApplicationScope.windowScopes().forEach {
TerminalPanelFactory.getInstance(it)
.fireResize()
}
} }
private fun initView() { private fun initView() {
@@ -489,7 +494,11 @@ class SettingsOptionsPane : OptionsPane() {
getTokenBtn.addActionListener { getTokenBtn.addActionListener {
when (typeComboBox.selectedItem) { when (typeComboBox.selectedItem) {
SyncType.GitLab -> Application.browse(URI.create("https://gitlab.com/-/user_settings/personal_access_tokens")) SyncType.GitLab -> {
val uri = URI.create(domainTextField.text)
Application.browse(URI.create("${uri.scheme}://${uri.host}/-/user_settings/personal_access_tokens?name=Termora%20Sync%20Config&scopes=api"))
}
SyncType.GitHub -> Application.browse(URI.create("https://github.com/settings/tokens")) SyncType.GitHub -> Application.browse(URI.create("https://github.com/settings/tokens"))
SyncType.Gitee -> Application.browse(URI.create("https://gitee.com/profile/personal_access_tokens")) SyncType.Gitee -> Application.browse(URI.create("https://gitee.com/profile/personal_access_tokens"))
} }
@@ -537,21 +546,21 @@ class SettingsOptionsPane : OptionsPane() {
put("os", SystemUtils.OS_NAME) put("os", SystemUtils.OS_NAME)
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now))) put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
if (syncConfig.ranges.contains(SyncRange.Hosts)) { if (syncConfig.ranges.contains(SyncRange.Hosts)) {
put("hosts", ohMyJson.encodeToJsonElement(HostManager.instance.hosts())) put("hosts", ohMyJson.encodeToJsonElement(HostManager.getInstance().hosts()))
} }
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) { if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.instance.getOhKeyPairs())) put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.getInstance().getOhKeyPairs()))
} }
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) { if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
put( put(
"keywordHighlights", "keywordHighlights",
ohMyJson.encodeToJsonElement(KeywordHighlightManager.instance.getKeywordHighlights()) ohMyJson.encodeToJsonElement(KeywordHighlightManager.getInstance().getKeywordHighlights())
) )
} }
if (syncConfig.ranges.contains(SyncRange.Macros)) { if (syncConfig.ranges.contains(SyncRange.Macros)) {
put( put(
"macros", "macros",
ohMyJson.encodeToJsonElement(MacroManager.instance.getMacros()) ohMyJson.encodeToJsonElement(MacroManager.getInstance().getMacros())
) )
} }
put("settings", buildJsonObject { put("settings", buildJsonObject {
@@ -670,7 +679,7 @@ class SettingsOptionsPane : OptionsPane() {
// sync // sync
val syncResult = kotlin.runCatching { val syncResult = kotlin.runCatching {
val syncer = SyncerProvider.instance.getSyncer(syncConfig.type) val syncer = SyncerProvider.getInstance().getSyncer(syncConfig.type)
if (push) { if (push) {
syncer.push(syncConfig) syncer.push(syncConfig)
} else { } else {
@@ -905,10 +914,10 @@ class SettingsOptionsPane : OptionsPane() {
private fun createHyperlink(url: String, text: String = url): Hyperlink { private fun createHyperlink(url: String, text: String = url): Hyperlink {
return Hyperlink(object : AnAction(text) { return Hyperlink(object : AnAction(text) {
override fun actionPerformed(evt: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
Application.browse(URI.create(url)) Application.browse(URI.create(url))
} }
}); })
} }
private fun initEvents() {} private fun initEvents() {}
@@ -934,9 +943,9 @@ class SettingsOptionsPane : OptionsPane() {
private val twoPasswordTextField = OutlinePasswordField(255) private val twoPasswordTextField = OutlinePasswordField(255)
private val tip = FlatLabel() private val tip = FlatLabel()
private val safeBtn = FlatButton() private val safeBtn = FlatButton()
private val doorman get() = Doorman.instance private val doorman get() = Doorman.getInstance()
private val hostManager get() = HostManager.instance private val hostManager get() = HostManager.getInstance()
private val keyManager get() = KeyManager.instance private val keyManager get() = KeyManager.getInstance()
init { init {
initView() initView()
@@ -1159,5 +1168,35 @@ class SettingsOptionsPane : OptionsPane() {
} }
private inner class KeyShortcutsOption : JPanel(BorderLayout()), Option {
private val keymapPanel = KeymapPanel()
init {
initView()
initEvents()
}
private fun initView() {
add(keymapPanel, BorderLayout.CENTER)
}
private fun initEvents() {}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.fitContent
}
override fun getTitle(): String {
return I18n.getString("termora.settings.keymap")
}
override fun getJComponent(): JComponent {
return this
}
}
} }

View File

@@ -1,18 +1,20 @@
package app.termora package app.termora
import app.termora.db.Database
import app.termora.terminal.* import app.termora.terminal.*
import app.termora.terminal.panel.TerminalPanel import app.termora.terminal.panel.TerminalPanel
import app.termora.tlog.TerminalLoggerDataListener import app.termora.tlog.TerminalLoggerDataListener
import java.awt.Color import java.awt.Color
import javax.swing.UIManager import javax.swing.UIManager
class TerminalFactory { class TerminalFactory private constructor() : Disposable {
private val terminals = mutableListOf<Terminal>() private val terminals = mutableListOf<Terminal>()
companion object { companion object {
val instance by lazy { TerminalFactory() } fun getInstance(scope: WindowScope): TerminalFactory {
return scope.getOrCreate(TerminalFactory::class) { TerminalFactory() }
} }
}
fun createTerminal(): Terminal { fun createTerminal(): Terminal {
val terminal = MyVisualTerminal() val terminal = MyVisualTerminal()
@@ -38,7 +40,7 @@ class TerminalFactory {
open class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) { open class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
private val colorPalette by lazy { MyColorPalette(terminal) } private val colorPalette by lazy { MyColorPalette(terminal) }
private val config get() = Database.instance.terminal private val config get() = Database.getDatabase().terminal
init { init {
this.setData(DataKey.CursorStyle, config.cursor) this.setData(DataKey.CursorStyle, config.cursor)
@@ -95,7 +97,7 @@ class TerminalFactory {
TerminalColor.Basic.SELECTION_FOREGROUND TerminalColor.Basic.SELECTION_FOREGROUND
) )
else -> DefaultColorTheme.instance.getColor(color) else -> DefaultColorTheme.getInstance().getColor(color)
} }
} }
@@ -108,4 +110,6 @@ class TerminalFactory {
return colorTheme return colorTheme
} }
} }
} }

View File

@@ -13,14 +13,16 @@ class TerminalPanelFactory {
private val terminalPanels = mutableListOf<TerminalPanel>() private val terminalPanels = mutableListOf<TerminalPanel>()
companion object { companion object {
val instance by lazy { TerminalPanelFactory() } fun getInstance(scope: Scope): TerminalPanelFactory {
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
}
} }
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel { fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
val terminalPanel = TerminalPanel(terminal, ptyConnector) val terminalPanel = TerminalPanel(terminal, ptyConnector)
terminalPanel.addTerminalPaintListener(MultipleTerminalListener()) terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.instance) terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.instance) terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
terminalPanels.add(terminalPanel) terminalPanels.add(terminalPanel)
return terminalPanel return terminalPanel
} }

View File

@@ -1,5 +1,9 @@
package app.termora package app.termora
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
import app.termora.terminal.DataKey
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
@@ -11,7 +15,9 @@ class TerminalTabDialog(
owner: Window, owner: Window,
size: Dimension, size: Dimension,
private val terminalTab: TerminalTab private val terminalTab: TerminalTab
) : DialogWrapper(null), Disposable { ) : DialogWrapper(null), Disposable, DataProvider {
private val dataProviderSupport = DataProviderSupport()
init { init {
title = terminalTab.getTitle() title = terminalTab.getTitle()
@@ -19,6 +25,7 @@ class TerminalTabDialog(
isAlwaysOnTop = false isAlwaysOnTop = false
iconImages = owner.iconImages iconImages = owner.iconImages
escapeDispose = false escapeDispose = false
processGlobalKeymap = true
super.setSize(size) super.setSize(size)
@@ -34,6 +41,13 @@ class TerminalTabDialog(
}) })
setLocationRelativeTo(null) setLocationRelativeTo(null)
if (owner is DataProvider) {
owner.getData(DataProviders.WindowScope)?.let {
dataProviderSupport.addData(DataProviders.WindowScope, it)
}
}
} }
override fun createSouthPanel(): JComponent? { override fun createSouthPanel(): JComponent? {
@@ -52,4 +66,8 @@ class TerminalTabDialog(
super<DialogWrapper>.dispose() super<DialogWrapper>.dispose()
} }
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
}
} }

View File

@@ -1,28 +1,35 @@
package app.termora package app.termora
import app.termora.actions.*
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhere
import app.termora.findeverywhere.FindEverywhereProvider import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereResult import app.termora.findeverywhere.FindEverywhereResult
import app.termora.terminal.DataKey
import app.termora.transport.TransportPanel import app.termora.transport.TransportPanel
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.jdesktop.swingx.action.ActionManager
import java.awt.* import java.awt.*
import java.awt.event.* import java.awt.event.AWTEventListener
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.beans.PropertyChangeListener import java.beans.PropertyChangeListener
import javax.swing.* import javax.swing.*
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
import kotlin.math.min import kotlin.math.min
class TerminalTabbed( class TerminalTabbed(
private val windowScope: WindowScope,
private val termoraToolBar: TermoraToolBar, private val termoraToolBar: TermoraToolBar,
private val tabbedPane: FlatTabbedPane, private val tabbedPane: FlatTabbedPane,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager { ) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>() private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener() private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
private val toolbar = termoraToolBar.getJToolBar() private val toolbar = termoraToolBar.getJToolBar()
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val iconListener = PropertyChangeListener { e -> private val iconListener = PropertyChangeListener { e ->
val source = e.source val source = e.source
@@ -52,6 +59,10 @@ class TerminalTabbed(
add(tabbedPane, BorderLayout.CENTER) add(tabbedPane, BorderLayout.CENTER)
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
dataProviderSupport.addData(DataProviders.TerminalTabbed, this)
dataProviderSupport.addData(DataProviders.TerminalTabbedManager, this)
} }
@@ -79,35 +90,6 @@ class TerminalTabbed(
} }
} }
// 快捷键
val inputMap = getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
val tabIndex = i - KeyEvent.VK_1 + 1
val actionKey = "select_$tabIndex"
actionMap.put(actionKey, object : AnAction() {
override fun actionPerformed(e: ActionEvent) {
tabbedPane.selectedIndex = if (i == KeyEvent.VK_9 || tabIndex > tabbedPane.tabCount) {
tabbedPane.tabCount - 1
} else {
tabIndex - 1
}
}
})
inputMap.put(KeyStroke.getKeyStroke(i, toolkit.menuShortcutKeyMaskEx), actionKey)
}
// 关闭 tab
actionMap.put("closeTab", object : AnAction() {
override fun actionPerformed(e: ActionEvent) {
if (tabbedPane.selectedIndex >= 0) {
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabbedPane.selectedIndex)
}
}
})
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "closeTab")
// 右键菜单 // 右键菜单
tabbedPane.addMouseListener(object : MouseAdapter() { tabbedPane.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { override fun mouseClicked(e: MouseEvent) {
@@ -136,7 +118,8 @@ class TerminalTabbed(
}) })
// 注册全局搜索 // 注册全局搜索
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider { FindEverywhereProvider.getFindEverywhereProviders(windowScope)
.add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
override fun find(pattern: String): List<FindEverywhereResult> { override fun find(pattern: String): List<FindEverywhereResult> {
val results = mutableListOf<FindEverywhereResult>() val results = mutableListOf<FindEverywhereResult>()
for (i in 0 until tabbedPane.tabCount) { for (i in 0 until tabbedPane.tabCount) {
@@ -165,16 +148,6 @@ class TerminalTabbed(
})) }))
// 打开 Host
ActionManager.getInstance().addAction(Actions.OPEN_HOST, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
if (e !is OpenHostActionEvent) {
return
}
openHost(e.host)
}
})
// 监听全局事件 // 监听全局事件
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK) toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
@@ -210,7 +183,9 @@ class TerminalTabbed(
private fun openHost(host: Host) { private fun openHost(host: Host) {
val tab = if (host.protocol == Protocol.SSH) SSHTerminalTab(host) else LocalTerminalTab(host) val tab = if (host.protocol == Protocol.SSH)
SSHTerminalTab(ApplicationScope.forWindowScope(this), host)
else LocalTerminalTab(ApplicationScope.forWindowScope(this), host)
addTab(tab) addTab(tab)
tab.start() tab.start()
} }
@@ -242,32 +217,34 @@ class TerminalTabbed(
// 克隆 // 克隆
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone")) val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
clone.addActionListener { clone.addActionListener { evt ->
if (tab is HostTerminalTab) { if (tab is HostTerminalTab) {
ActionManager.getInstance() actionManager
.getAction(Actions.OPEN_HOST) .getAction(OpenHostAction.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host)) .actionPerformed(OpenHostActionEvent(this, tab.host, evt))
} }
} }
// 在新窗口中打开 // 在新窗口中打开
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window")) val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
openInNewWindow.addActionListener { openInNewWindow.addActionListener(object : AnAction() {
val index = tabbedPane.selectedIndex override fun actionPerformed(evt: AnActionEvent) {
if (index > 0) { val owner = evt.getData(DataProviders.TermoraFrame) ?: return
val title = tabbedPane.getTitleAt(index) if (tabIndex > 0) {
removeTabAt(index, false) val title = tabbedPane.getTitleAt(tabIndex)
removeTabAt(tabIndex, false)
val dialog = TerminalTabDialog( val dialog = TerminalTabDialog(
owner = SwingUtilities.getWindowAncestor(this), owner = owner,
terminalTab = tab, terminalTab = tab,
size = Dimension(min(size.width, 1280), min(size.height, 800)) size = Dimension(min(size.width, 1280), min(size.height, 800))
) )
dialog.title = title dialog.title = title
Disposer.register(dialog, tab) Disposer.register(dialog, tab)
Disposer.register(this, dialog) Disposer.register(this@TerminalTabbed, dialog)
dialog.isVisible = true dialog.isVisible = true
} }
} }
})
popupMenu.addSeparator() popupMenu.addSeparator()
@@ -451,5 +428,18 @@ class TerminalTabbed(
} }
} }
override fun closeTerminalTab(tab: TerminalTab) {
for (i in 0 until tabs.size) {
if (tabs[i] == tab) {
removeTabAt(i, true)
break
}
}
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
}
} }

View File

@@ -5,4 +5,5 @@ interface TerminalTabbedManager {
fun getSelectedTerminalTab(): TerminalTab? fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<TerminalTab> fun getTerminalTabs(): List<TerminalTab>
fun setSelectedTerminalTab(tab: TerminalTab) fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab)
} }

View File

@@ -1,84 +1,53 @@
package app.termora package app.termora
import app.termora.findeverywhere.FindEverywhere
import app.termora.highlight.KeywordHighlightDialog import app.termora.actions.ActionManager
import app.termora.keymgr.KeyManagerDialog import app.termora.actions.DataProvider
import app.termora.macro.MacroAction import app.termora.actions.DataProviderSupport
import app.termora.tlog.TerminalLoggerAction import app.termora.actions.DataProviders
import app.termora.transport.SFTPAction import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatDesktop
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR import com.jetbrains.JBR
import io.github.g00fy2.versioncompare.Version
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory
import java.awt.Dimension import java.awt.Dimension
import java.awt.Insets import java.awt.Insets
import java.awt.KeyEventDispatcher
import java.awt.KeyboardFocusManager import java.awt.KeyboardFocusManager
import java.awt.event.* import java.awt.event.MouseAdapter
import java.net.URI import java.awt.event.MouseEvent
import java.util.*
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.swing.* import javax.swing.Box
import javax.swing.JFrame
import javax.swing.SwingUtilities
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.event.HyperlinkEvent import javax.swing.UIManager
import kotlin.concurrent.fixedRateTimer
import kotlin.math.max import kotlin.math.max
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
fun assertEventDispatchThread() { fun assertEventDispatchThread() {
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue") if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
} }
class TermoraFrame : JFrame() { class TermoraFrame : JFrame(), DataProvider {
companion object {
private val log = LoggerFactory.getLogger(TermoraFrame::class.java)
}
private val actionManager get() = ActionManager.getInstance()
private val id = UUID.randomUUID().toString()
private val windowScope = ApplicationScope.forWindowScope(this)
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this) private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
private val tabbedPane = MyTabbedPane() private val tabbedPane = MyTabbedPane()
private val toolbar = TermoraToolBar(titleBar, tabbedPane) private val toolbar = TermoraToolBar(titleBar, tabbedPane)
private lateinit var terminalTabbed: TerminalTabbed private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
private val disposable = Disposer.newDisposable()
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() } private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
private val updaterManager get() = UpdaterManager.instance private val dataProviderSupport = DataProviderSupport()
private val welcomePanel = WelcomePanel(windowScope)
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
private val preferencesHandler = object : Runnable {
override fun run() {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: this@TermoraFrame
if (owner != this@TermoraFrame) {
return
}
val that = this
FlatDesktop.setPreferencesHandler {}
val dialog = SettingsDialog(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
FlatDesktop.setPreferencesHandler(that)
}
})
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
}
}
init { init {
initActions()
initView() initView()
initEvents() initEvents()
initDesktopHandler()
scheduleUpdate()
} }
private fun initEvents() { private fun initEvents() {
@@ -97,154 +66,19 @@ class TermoraFrame : JFrame() {
} }
} }
// global shortcuts
rootPane.actionMap.put(Actions.FIND_EVERYWHERE, ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE))
rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, toolkit.menuShortcutKeyMaskEx), Actions.FIND_EVERYWHERE)
// double shift
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(object : KeyEventDispatcher {
private var lastTime = -1L
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
val now = System.currentTimeMillis()
if (now - 250 < lastTime) {
ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE)
.actionPerformed(ActionEvent(rootPane, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
}
lastTime = now
} else if (e.keyCode != KeyEvent.VK_SHIFT) { // 如果不是 Shift 键,那么就阻断了连续性,重置时间
lastTime = -1
}
return false
}
})
// 监听主题变化 需要动态修改控制栏颜色 // 监听主题变化 需要动态修改控制栏颜色
if (SystemInfo.isWindows && isWindowDecorationsSupported) { if (SystemInfo.isWindows && isWindowDecorationsSupported) {
ThemeManager.instance.addThemeChangeListener(object : ThemeChangeListener { ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
override fun onChanged() { override fun onChanged() {
titleBar.putProperty("controls.dark", FlatLaf.isLafDark()) titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
} }
}) })
} }
// dispose
addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
Disposer.dispose(disposable)
Disposer.dispose(ApplicationDisposable.instance)
try {
Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) {
log.error(e.message)
}
exitProcess(0)
}
})
} }
private fun initActions() {
// SETTING
ActionManager.getInstance().addAction(Actions.SETTING, object : AnAction(
I18n.getString("termora.setting"),
Icons.settings
) {
override fun actionPerformed(e: ActionEvent) {
preferencesHandler.run()
}
})
// MULTIPLE
ActionManager.getInstance().addAction(Actions.MULTIPLE, object : AnAction(
I18n.getString("termora.tools.multiple"),
Icons.vcs
) {
init {
setStateAction()
}
override fun actionPerformed(evt: ActionEvent) {
TerminalPanelFactory.instance.repaintAll()
}
})
// Keyword Highlight
ActionManager.getInstance().addAction(Actions.KEYWORD_HIGHLIGHT, object : AnAction(
I18n.getString("termora.highlight"),
Icons.edit
) {
override fun actionPerformed(evt: ActionEvent) {
KeywordHighlightDialog(this@TermoraFrame).isVisible = true
}
})
// app update
ActionManager.getInstance().addAction(Actions.APP_UPDATE, object :
AnAction(
StringUtils.EMPTY,
Icons.ideUpdate
) {
init {
isEnabled = false
}
override fun actionPerformed(evt: ActionEvent) {
showUpdateDialog()
}
})
// 终端日志记录
ActionManager.getInstance().addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
// SFTP
ActionManager.getInstance().addAction(Actions.SFTP, SFTPAction())
// macro
ActionManager.getInstance().addAction(Actions.MACRO, MacroAction())
// FIND_EVERYWHERE
ActionManager.getInstance().addAction(Actions.FIND_EVERYWHERE, object : AnAction(
I18n.getString("termora.find-everywhere"),
Icons.find
) {
override fun actionPerformed(evt: ActionEvent) {
if (this.isEnabled) {
val focusWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
val frame = this@TermoraFrame
if (focusWindow == frame) {
val dialog = FindEverywhere(frame)
dialog.setLocationRelativeTo(frame)
dialog.isVisible = true
}
}
}
})
// Key manager
ActionManager.getInstance().addAction(Actions.KEY_MANAGER, object : AnAction(
I18n.getString("termora.keymgr.title"),
Icons.greyKey
) {
override fun actionPerformed(evt: ActionEvent) {
if (this.isEnabled) {
KeyManagerDialog(this@TermoraFrame).isVisible = true
}
}
})
}
private fun initView() { private fun initView() {
if (isWindowDecorationsSupported) { if (isWindowDecorationsSupported) {
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat() titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
@@ -267,10 +101,7 @@ class TermoraFrame : JFrame() {
} }
minimumSize = Dimension(640, 400) minimumSize = Dimension(640, 400)
terminalTabbed = TerminalTabbed(toolbar, tabbedPane).apply { terminalTabbed.addTab(welcomePanel)
Application.registerService(TerminalTabbedManager::class, this)
}
terminalTabbed.addTab(WelcomePanel())
// macOS 要避开左边的控制栏 // macOS 要避开左边的控制栏
if (SystemInfo.isMacOS) { if (SystemInfo.isMacOS) {
@@ -282,89 +113,13 @@ class TermoraFrame : JFrame() {
} }
} }
Disposer.register(disposable, terminalTabbed) Disposer.register(windowScope, terminalTabbed)
add(terminalTabbed) add(terminalTabbed)
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
} }
private fun showUpdateDialog() {
val lastVersion = updaterManager.lastVersion
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = lastVersion.htmlBody
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
scrollPane.preferredSize = Dimension(
UIManager.getInt("Dialog.width") - 100,
UIManager.getInt("Dialog.height") - 100
)
val option = OptionPane.showConfirmDialog(
this,
scrollPane,
title = I18n.getString("termora.update.title"),
messageType = JOptionPane.PLAIN_MESSAGE,
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
options = arrayOf(
I18n.getString("termora.update.update"),
I18n.getString("termora.update.ignore"),
I18n.getString("termora.cancel")
),
initialValue = I18n.getString("termora.update.update")
)
if (option == JOptionPane.CANCEL_OPTION) {
return
} else if (option == JOptionPane.NO_OPTION) {
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
updaterManager.ignore(updaterManager.lastVersion.version)
} else if (option == JOptionPane.YES_OPTION) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, false)
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun scheduleUpdate() {
fixedRateTimer(
name = "check-update-timer",
initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true
) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
}
}
private suspend fun checkUpdate() {
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
val newVersion = Version(latestVersion.version)
val version = Version(Application.getVersion())
if (newVersion <= version) {
return
}
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
withContext(Dispatchers.Swing) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, true)
}
}
private fun forceHitTest() { private fun forceHitTest() {
val mouseAdapter = object : MouseAdapter() { val mouseAdapter = object : MouseAdapter() {
@@ -423,11 +178,25 @@ class TermoraFrame : JFrame() {
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter) toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
} }
private fun initDesktopHandler() { override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (SystemInfo.isMacOS) { return dataProviderSupport.getData(dataKey)
FlatDesktop.setPreferencesHandler { ?: terminalTabbed.getData(dataKey)
preferencesHandler.run() ?: welcomePanel.getData(dataKey)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TermoraFrame
return id == other.id
} }
override fun hashCode(): Int {
return id.hashCode()
} }
} }

View File

@@ -0,0 +1,60 @@
package app.termora
import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
import kotlin.system.exitProcess
class TermoraFrameManager {
companion object {
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
fun getInstance(): TermoraFrameManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(TermoraFrameManager::class) { TermoraFrameManager() }
}
}
fun createWindow(): TermoraFrame {
val frame = TermoraFrame()
registerCloseCallback(frame)
frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
return frame
}
private fun registerCloseCallback(window: TermoraFrame) {
window.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
// dispose windowScope
Disposer.dispose(ApplicationScope.forWindowScope(e.window))
val windowScopes = ApplicationScope.windowScopes()
// 如果已经没有 Window 域了,那么就可以退出程序了
if (windowScopes.isEmpty()) {
this@TermoraFrameManager.dispose()
}
}
})
}
private fun dispose() {
Disposer.dispose(ApplicationScope.forApplicationScope())
try {
Disposer.getTree().assertIsEmpty(true)
} catch (e: Exception) {
log.error(e.message)
}
exitProcess(0)
}
}

View File

@@ -1,16 +1,18 @@
package app.termora package app.termora
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.db.Database import app.termora.actions.ActionManager
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.SettingsAction
import app.termora.findeverywhere.FindEverywhereAction
import com.formdev.flatlaf.extras.components.FlatTabbedPane import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.WindowDecorations import com.jetbrains.WindowDecorations
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionContainerFactory import org.jdesktop.swingx.action.ActionContainerFactory
import org.jdesktop.swingx.action.ActionManager
import java.awt.Insets import java.awt.Insets
import java.awt.event.ActionEvent
import java.awt.event.ComponentAdapter import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import javax.swing.Box import javax.swing.Box
@@ -27,7 +29,7 @@ class TermoraToolBar(
private val titleBar: WindowDecorations.CustomTitleBar, private val titleBar: WindowDecorations.CustomTitleBar,
private val tabbedPane: FlatTabbedPane private val tabbedPane: FlatTabbedPane
) { ) {
private val properties by lazy { Database.instance.properties } private val properties by lazy { Database.getDatabase().properties }
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } } private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
@@ -46,8 +48,8 @@ class TermoraToolBar(
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true), ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
ToolBarAction(Actions.KEY_MANAGER, true), ToolBarAction(Actions.KEY_MANAGER, true),
ToolBarAction(Actions.MULTIPLE, true), ToolBarAction(Actions.MULTIPLE, true),
ToolBarAction(Actions.FIND_EVERYWHERE, true), ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
ToolBarAction(Actions.SETTING, true), ToolBarAction(SettingsAction.SETTING, true),
) )
} }
@@ -96,12 +98,12 @@ class TermoraToolBar(
toolbar.removeAll() toolbar.removeAll()
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) { toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
override fun actionPerformed(e: ActionEvent?) { override fun actionPerformed(evt: AnActionEvent) {
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e) actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
} }
override fun isEnabled(): Boolean { override fun isEnabled(): Boolean {
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
} }
})) }))

View File

@@ -1,6 +1,5 @@
package app.termora package app.termora
import app.termora.db.Database
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatAnimatedLafChange import com.formdev.flatlaf.extras.FlatAnimatedLafChange
import com.jthemedetecor.OsThemeDetector import com.jthemedetecor.OsThemeDetector
@@ -24,7 +23,9 @@ class ThemeManager private constructor() {
companion object { companion object {
private val log = LoggerFactory.getLogger(ThemeManager::class.java) private val log = LoggerFactory.getLogger(ThemeManager::class.java)
val instance by lazy { ThemeManager() } fun getInstance(): ThemeManager {
return ApplicationScope.forApplicationScope().getOrCreate(ThemeManager::class) { ThemeManager() }
}
} }
val themes = mapOf( val themes = mapOf(
@@ -78,7 +79,7 @@ class ThemeManager private constructor() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> { OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
override fun accept(isDark: Boolean) { override fun accept(isDark: Boolean) {
if (!Database.instance.appearance.followSystem) { if (!Database.getDatabase().appearance.followSystem) {
return return
} }

View File

@@ -1,7 +1,6 @@
package app.termora package app.termora
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.db.Database
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.Request import okhttp3.Request
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
@@ -19,7 +18,9 @@ import java.util.*
class UpdaterManager private constructor() { class UpdaterManager private constructor() {
companion object { companion object {
private val log = LoggerFactory.getLogger(UpdaterManager::class.java) private val log = LoggerFactory.getLogger(UpdaterManager::class.java)
val instance by lazy { UpdaterManager() } fun getInstance(): UpdaterManager {
return ApplicationScope.forApplicationScope().getOrCreate(UpdaterManager::class) { UpdaterManager() }
}
} }
data class Asset( data class Asset(
@@ -58,7 +59,7 @@ class UpdaterManager private constructor() {
val isSelf get() = this == self val isSelf get() = this == self
} }
private val properties get() = Database.instance.properties private val properties get() = Database.getDatabase().properties
var lastVersion = LatestVersion.self var lastVersion = LatestVersion.self
fun fetchLatestVersion(): LatestVersion { fun fetchLatestVersion(): LatestVersion {

View File

@@ -1,10 +1,11 @@
package app.termora package app.termora
import app.termora.db.Database
import app.termora.actions.*
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhere
import app.termora.findeverywhere.FindEverywhereProvider import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereResult import app.termora.findeverywhere.FindEverywhereResult
import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
@@ -19,17 +20,18 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import javax.swing.* import javax.swing.*
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import javax.swing.tree.TreePath
import kotlin.math.max import kotlin.math.max
class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab { class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab,
private val properties get() = Database.instance.properties DataProvider {
private val properties get() = Database.getDatabase().properties
private val rootPanel = JPanel(BorderLayout()) private val rootPanel = JPanel(BorderLayout())
private val searchTextField = FlatTextField() private val searchTextField = FlatTextField()
private val hostTree = HostTree() private val hostTree = HostTree()
private val bannerPanel = BannerPanel() private val bannerPanel = BannerPanel()
private val toggle = FlatButton() private val toggle = FlatButton()
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean() private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
private val dataProviderSupport = DataProviderSupport()
init { init {
initView() initView()
@@ -51,6 +53,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
rootPanel.add(panel, BorderLayout.CENTER) rootPanel.add(panel, BorderLayout.CENTER)
add(rootPanel, BorderLayout.CENTER) add(rootPanel, BorderLayout.CENTER)
dataProviderSupport.addData(DataProviders.Welcome.HostTree, hostTree)
} }
@@ -73,7 +76,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
newHost.isFocusable = false newHost.isFocusable = false
newHost.buttonType = FlatButton.ButtonType.toolBarButton newHost.buttonType = FlatButton.ButtonType.toolBarButton
newHost.addActionListener { e -> newHost.addActionListener { e ->
ActionManager.getInstance().getAction(Actions.ADD_HOST)?.actionPerformed(e) ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)?.actionPerformed(e)
} }
@@ -117,7 +120,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
private fun createHostPanel(): JComponent { private fun createHostPanel(): JComponent {
val panel = JPanel(BorderLayout()) val panel = JPanel(BorderLayout())
hostTree.actionMap.put("find", object : AnAction() { hostTree.actionMap.put("find", object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
searchTextField.requestFocusInWindow() searchTextField.requestFocusInWindow()
} }
}) })
@@ -160,16 +163,8 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
}) })
ActionManager.getInstance().addAction(Actions.ADD_HOST, object : AnAction() { FindEverywhereProvider.getFindEverywhereProviders(windowScope)
override fun actionPerformed(e: ActionEvent) { .add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
if (hostTree.selectionCount < 1) {
hostTree.selectionPath = TreePath(hostTree.model.root)
}
hostTree.showAddHostDialog()
}
})
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
override fun find(pattern: String): List<FindEverywhereResult> { override fun find(pattern: String): List<FindEverywhereResult> {
return TreeUtils.children(hostTree.model, hostTree.model.root) return TreeUtils.children(hostTree.model, hostTree.model.root)
.filterIsInstance<Host>() .filterIsInstance<Host>()
@@ -240,8 +235,8 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult { private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
ActionManager.getInstance() ActionManager.getInstance()
.getAction(Actions.OPEN_HOST) .getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, host)) ?.actionPerformed(OpenHostActionEvent(e.source, host, e))
} }
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
@@ -258,5 +253,9 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
} }
} }
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
}
} }

View File

@@ -0,0 +1,71 @@
package app.termora.actions
import app.termora.Actions
import app.termora.ApplicationScope
import app.termora.findeverywhere.FindEverywhereAction
import app.termora.highlight.KeywordHighlightAction
import app.termora.keymgr.KeyManagerAction
import app.termora.macro.MacroAction
import app.termora.tlog.TerminalLoggerAction
import app.termora.transport.SFTPAction
import javax.swing.Action
class ActionManager : org.jdesktop.swingx.action.ActionManager() {
companion object {
fun getInstance(): ActionManager {
return ApplicationScope.forApplicationScope().getOrCreate(ActionManager::class) { ActionManager() }
}
}
init {
setInstance(this)
registerActions()
}
private fun registerActions() {
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
addAction(Actions.MULTIPLE, MultipleAction())
addAction(Actions.APP_UPDATE, AppUpdateAction())
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
addAction(Actions.SFTP, SFTPAction())
addAction(Actions.MACRO, MacroAction())
addAction(Actions.KEY_MANAGER, KeyManagerAction())
addAction(SwitchTabAction.SWITCH_TAB, SwitchTabAction())
addAction(SettingsAction.SETTING, SettingsAction())
addAction(NewHostAction.NEW_HOST, NewHostAction())
addAction(OpenHostAction.OPEN_HOST, OpenHostAction())
addAction(TerminalCopyAction.COPY, TerminalCopyAction())
addAction(TerminalPasteAction.PASTE, TerminalPasteAction())
addAction(TerminalFindAction.FIND, TerminalFindAction())
addAction(TerminalCloseAction.CLOSE, TerminalCloseAction())
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())
addAction(TerminalZoomResetAction.ZOOM_RESET, TerminalZoomResetAction())
}
override fun addAction(action: Action): Action {
val actionId = action.getValue(Action.ACTION_COMMAND_KEY) ?: throw IllegalArgumentException("Invalid action ID")
return addAction(actionId, action)
}
override fun addAction(id: Any, action: Action): Action {
if (getAction(id) != null) {
throw IllegalArgumentException("Action already exists")
}
return super.addAction(id, action)
}
}

View File

@@ -0,0 +1,30 @@
package app.termora.actions
import org.jdesktop.swingx.action.BoundAction
import java.awt.event.ActionEvent
import javax.swing.Icon
abstract class AnAction : BoundAction {
constructor() : super()
constructor(icon: Icon) : super() {
super.putValue(SMALL_ICON, icon)
}
constructor(name: String?) : super(name)
constructor(name: String?, icon: Icon?) : super(name, icon)
final override fun actionPerformed(evt: ActionEvent) {
if (evt is AnActionEvent) {
actionPerformed(evt)
} else {
actionPerformed(AnActionEvent(evt.source, evt.actionCommand, evt))
}
}
protected abstract fun actionPerformed(evt: AnActionEvent)
}

View File

@@ -0,0 +1,61 @@
package app.termora.actions
import app.termora.terminal.DataKey
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.Window
import java.awt.event.ActionEvent
import java.util.*
import javax.swing.JPopupMenu
open class AnActionEvent(
source: Any, command: String,
val event: EventObject
) : ActionEvent(source, AN_ACTION_PERFORMED, command), DataProvider {
companion object {
const val AN_ACTION_PERFORMED = ACTION_PERFORMED + 1
}
val window: Window
get() = getData(DataProviders.TermoraFrame)
?: KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
public override fun consume() {
super.consumed = true
}
public override fun isConsumed(): Boolean {
return super.isConsumed()
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
val source = getSource()
if (source !is Component) {
if (source is DataProvider) {
return source.getData(dataKey)
}
return null
} else {
var c = source as Component?
while (c != null) {
if (c is DataProvider) {
val data = c.getData(dataKey)
if (data != null) {
return data
}
}
val p = c.parent
c = if (p == null && c is JPopupMenu) {
c.invoker
} else {
p
}
}
return null
}
}
}

View File

@@ -0,0 +1,117 @@
package app.termora.actions
import app.termora.*
import io.github.g00fy2.versioncompare.Version
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.net.URI
import javax.swing.BorderFactory
import javax.swing.JOptionPane
import javax.swing.JScrollPane
import javax.swing.UIManager
import javax.swing.event.HyperlinkEvent
import kotlin.concurrent.fixedRateTimer
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
class AppUpdateAction : AnAction(
StringUtils.EMPTY,
Icons.ideUpdate
) {
private val updaterManager get() = UpdaterManager.getInstance()
init {
isEnabled = false
scheduleUpdate()
}
override fun actionPerformed(evt: AnActionEvent) {
showUpdateDialog()
}
@OptIn(DelicateCoroutinesApi::class)
private fun scheduleUpdate() {
fixedRateTimer(
name = "check-update-timer",
initialDelay = 3.minutes.inWholeMilliseconds,
period = 5.hours.inWholeMilliseconds, daemon = true
) {
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
}
}
private suspend fun checkUpdate() {
val latestVersion = updaterManager.fetchLatestVersion()
if (latestVersion.isSelf) {
return
}
val newVersion = Version(latestVersion.version)
val version = Version(Application.getVersion())
if (newVersion <= version) {
return
}
if (updaterManager.isIgnored(latestVersion.version)) {
return
}
withContext(Dispatchers.Swing) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, true)
}
}
private fun showUpdateDialog() {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
val lastVersion = updaterManager.lastVersion
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = lastVersion.htmlBody
editorPane.isEditable = false
editorPane.addHyperlinkListener {
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
Application.browse(it.url.toURI())
}
}
editorPane.background = DynamicColor("window")
val scrollPane = JScrollPane(editorPane)
scrollPane.border = BorderFactory.createEmptyBorder()
scrollPane.preferredSize = Dimension(
UIManager.getInt("Dialog.width") - 100,
UIManager.getInt("Dialog.height") - 100
)
val option = OptionPane.showConfirmDialog(
owner,
scrollPane,
title = I18n.getString("termora.update.title"),
messageType = JOptionPane.PLAIN_MESSAGE,
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
options = arrayOf(
I18n.getString("termora.update.update"),
I18n.getString("termora.update.ignore"),
I18n.getString("termora.cancel")
),
initialValue = I18n.getString("termora.update.update")
)
if (option == JOptionPane.CANCEL_OPTION) {
return
} else if (option == JOptionPane.NO_OPTION) {
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
updaterManager.ignore(updaterManager.lastVersion.version)
} else if (option == JOptionPane.YES_OPTION) {
ActionManager.getInstance()
.setEnabled(Actions.APP_UPDATE, false)
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
}
}
}

View File

@@ -0,0 +1,21 @@
package app.termora.actions
import app.termora.terminal.DataKey
/**
* 数据提供者,从 [AnActionEvent.source] 开始搜索然后依次 [getData] 获取数据
*/
interface DataProvider {
companion object {
val EMPTY = object : DataProvider {
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return null
}
}
}
/**
* 数据提供
*/
fun <T : Any> getData(dataKey: DataKey<T>): T?
}

View File

@@ -0,0 +1,24 @@
package app.termora.actions
import app.termora.terminal.DataKey
class DataProviderSupport : DataProvider {
private val map = mutableMapOf<DataKey<*>, Any>()
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (map.containsKey(dataKey)) {
@Suppress("UNCHECKED_CAST")
return map[dataKey] as T
}
return null
}
fun <T : Any> addData(key: DataKey<T>, data: T) {
map[key] = data
}
fun removeData(key: DataKey<*>) {
map.remove(key)
}
}

View File

@@ -0,0 +1,18 @@
package app.termora.actions
import app.termora.terminal.DataKey
object DataProviders {
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
val Terminal = DataKey(app.termora.terminal.Terminal::class)
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
val TermoraFrame = DataKey(app.termora.TermoraFrame::class)
val WindowScope = DataKey(app.termora.WindowScope::class)
object Welcome {
val HostTree = DataKey(app.termora.HostTree::class)
}
}

View File

@@ -0,0 +1,17 @@
package app.termora.actions
import app.termora.*
class MultipleAction : AnAction(
I18n.getString("termora.tools.multiple"),
Icons.vcs
) {
init {
setStateAction()
}
override fun actionPerformed(evt: AnActionEvent) {
ApplicationScope.windowScopes().map { TerminalPanelFactory.getInstance(it) }
.forEach { it.repaintAll() }
}
}

View File

@@ -0,0 +1,46 @@
package app.termora.actions
import app.termora.Host
import app.termora.HostDialog
import app.termora.HostManager
import app.termora.Protocol
import javax.swing.tree.TreePath
class NewHostAction : AnAction() {
companion object {
/**
* 添加主机对话框
*/
const val NEW_HOST = "NewHostAction"
}
private val hostManager get() = HostManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
val model = tree.model
var lastHost = tree.lastSelectedPathComponent ?: model.root
if (lastHost !is Host) {
return
}
if (lastHost.protocol != Protocol.Folder) {
val p = model.getParent(lastHost) ?: return
lastHost = p
}
val dialog = HostDialog(evt.window)
dialog.setLocationRelativeTo(evt.window)
dialog.isVisible = true
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
hostManager.addHost(host)
tree.expandNode(lastHost)
tree.selectionPath = TreePath(model.getPathToRoot(host))
}
}

View File

@@ -0,0 +1,27 @@
package app.termora.actions
import app.termora.I18n
import app.termora.TermoraFrameManager
import java.awt.KeyboardFocusManager
class NewWindowAction : AnAction() {
companion object {
/**
* 打开一个新的窗口
*/
const val NEW_WINDOW = "NewWindowAction"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-new-window"))
putValue(ACTION_COMMAND_KEY, NEW_WINDOW)
}
override fun actionPerformed(evt: AnActionEvent) {
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
if (focusedWindow == evt.window) {
TermoraFrameManager.getInstance().createWindow().isVisible = true
}
}
}

View File

@@ -0,0 +1,28 @@
package app.termora.actions
import app.termora.LocalTerminalTab
import app.termora.OpenHostActionEvent
import app.termora.Protocol
import app.termora.SSHTerminalTab
class OpenHostAction : AnAction() {
companion object {
/**
* 打开一个主机
*/
const val OPEN_HOST = "OpenHostAction"
}
override fun actionPerformed(evt: AnActionEvent) {
if (evt !is OpenHostActionEvent) return
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
val tab = if (evt.host.protocol == Protocol.SSH)
SSHTerminalTab(windowScope, evt.host)
else LocalTerminalTab(windowScope, evt.host)
terminalTabbedManager.addTerminalTab(tab)
tab.start()
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.actions
import app.termora.*
class OpenLocalTerminalAction : AnAction(
I18n.getString("termora.find-everywhere.quick-command.local-terminal"),
Icons.terminal
) {
companion object {
const val LOCAL_TERMINAL = "OpenLocalTerminal"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-local-terminal"))
putValue(ACTION_COMMAND_KEY, LOCAL_TERMINAL)
}
override fun actionPerformed(evt: AnActionEvent) {
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)?.actionPerformed(
OpenHostActionEvent(
evt.source,
Host(
name = name,
protocol = Protocol.Local
),
evt
)
)
evt.consume()
}
}

View File

@@ -0,0 +1,53 @@
package app.termora.actions
import app.termora.I18n
import app.termora.Icons
import app.termora.SettingsDialog
import com.formdev.flatlaf.extras.FlatDesktop
import org.apache.commons.lang3.StringUtils
import java.awt.KeyboardFocusManager
import java.awt.event.ActionEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
class SettingsAction : AnAction(
I18n.getString("termora.setting"),
Icons.settings
) {
companion object {
/**
* 打开设置
*/
const val SETTING = "SettingAction"
}
private var isShowing = false
init {
FlatDesktop.setPreferencesHandler {
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
if (owner != null) {
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
}
}
}
override fun actionPerformed(evt: AnActionEvent) {
if (isShowing) {
return
}
isShowing = true
val owner = evt.window
val dialog = SettingsDialog(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
this@SettingsAction.isShowing = false
}
})
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.actions
import app.termora.I18n
import java.awt.event.KeyEvent
class SwitchTabAction : AnAction() {
companion object {
const val SWITCH_TAB = "SwitchTabAction"
}
init {
putValue(ACTION_COMMAND_KEY, SWITCH_TAB)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.switch-tab"))
}
override fun actionPerformed(evt: AnActionEvent) {
val original = evt.event
if (original !is KeyEvent) return
if (original.keyCode !in KeyEvent.VK_1..KeyEvent.VK_9) return
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
val tabs = terminalTabbedManager.getTerminalTabs()
if (tabs.isEmpty()) return
val tabIndex = original.keyCode - KeyEvent.VK_1
if (tabIndex >= tabs.size) {
terminalTabbedManager.setSelectedTerminalTab(tabs.last())
} else {
terminalTabbedManager.setSelectedTerminalTab(tabs[tabIndex])
}
evt.consume()
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.actions
class TerminalClearScreenAction : AnAction() {
companion object {
const val CLEAR_SCREEN = "ClearScreen"
}
init {
putValue(SHORT_DESCRIPTION, "Clear Terminal Buffer")
putValue(ACTION_COMMAND_KEY, CLEAR_SCREEN)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminal = evt.getData(DataProviders.Terminal) ?: return
terminal.getDocument().eraseInDisplay(3)
evt.consume()
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.actions
import app.termora.I18n
class TerminalCloseAction : AnAction() {
companion object {
const val CLOSE = "Close"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.close-tab"))
putValue(ACTION_COMMAND_KEY, CLOSE)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
terminalTabbedManager.getSelectedTerminalTab()?.let {
terminalTabbedManager.closeTerminalTab(it)
evt.consume()
}
}
}

View File

@@ -1,25 +1,29 @@
package app.termora.terminal.panel package app.termora.actions
import app.termora.I18n import app.termora.I18n
import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.Transferable import java.awt.datatransfer.Transferable
import java.awt.datatransfer.UnsupportedFlavorException import java.awt.datatransfer.UnsupportedFlavorException
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalPredicateAction { class TerminalCopyAction : AnAction() {
companion object { companion object {
const val COPY = "TerminalCopy"
private val log = LoggerFactory.getLogger(TerminalCopyAction::class.java) private val log = LoggerFactory.getLogger(TerminalCopyAction::class.java)
} }
private val systemClipboard get() = terminalPanel.toolkit.systemClipboard init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.copy-from-terminal"))
putValue(ACTION_COMMAND_KEY, COPY)
}
override fun actionPerformed(e: KeyEvent) { override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
val text = terminalPanel.copy() val text = terminalPanel.copy()
val systemClipboard = terminalPanel.toolkit.systemClipboard
evt.consume()
// 如果文本为空,那么清空剪切板 // 如果文本为空,那么清空剪切板
if (text.isEmpty()) { if (text.isEmpty()) {
@@ -30,22 +34,10 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalPre
systemClipboard.setContents(StringSelection(text), null) systemClipboard.setContents(StringSelection(text), null)
terminalPanel.toast(I18n.getString("termora.terminal.copied")) terminalPanel.toast(I18n.getString("termora.terminal.copied"))
if (log.isTraceEnabled) { if (log.isTraceEnabled) {
log.info("Copy to clipboard. {}", text) log.trace("Copy to clipboard. {}", text)
} }
} }
override fun test(keyStroke: KeyStroke, e: KeyEvent): Boolean {
if (SystemInfo.isMacOS) {
return KeyStroke.getKeyStroke(KeyEvent.VK_C, terminalPanel.toolkit.menuShortcutKeyMaskEx) == keyStroke
}
// Ctrl + Insert
val keyStroke1 = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.CTRL_DOWN_MASK)
// Ctrl + Shift + C
val keyStroke2 = KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK)
return keyStroke == keyStroke1 || keyStroke == keyStroke2
}
private class EmptyTransferable : Transferable { private class EmptyTransferable : Transferable {
override fun getTransferDataFlavors(): Array<DataFlavor> { override fun getTransferDataFlavors(): Array<DataFlavor> {
@@ -61,4 +53,5 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalPre
} }
} }
} }

View File

@@ -0,0 +1,23 @@
package app.termora.actions
import app.termora.I18n
class TerminalFindAction : AnAction() {
companion object {
const val FIND = "TerminalFind"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-terminal-find"))
putValue(ACTION_COMMAND_KEY, FIND)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
terminalPanel.showFind()
evt.consume()
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.actions
import app.termora.I18n
import org.slf4j.LoggerFactory
import java.awt.datatransfer.DataFlavor
class TerminalPasteAction : AnAction() {
companion object {
const val PASTE = "TerminalPaste"
private val log = LoggerFactory.getLogger(TerminalPasteAction::class.java)
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.paste-to-terminal"))
putValue(ACTION_COMMAND_KEY, PASTE)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
val systemClipboard = terminalPanel.toolkit.systemClipboard
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
val text = systemClipboard.getData(DataFlavor.stringFlavor)
if (text is String) {
terminalPanel.paste(text)
if (log.isTraceEnabled) {
log.info("Paste {}", text)
}
}
}
evt.consume()
}
}

View File

@@ -0,0 +1,25 @@
package app.termora.actions
import app.termora.I18n
import app.termora.terminal.Position
class TerminalSelectAllAction : AnAction() {
companion object {
const val SELECT_ALL = "TerminalSelectAll"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.select-all-in-terminal"))
putValue(ACTION_COMMAND_KEY, SELECT_ALL)
}
override fun actionPerformed(evt: AnActionEvent) {
val terminal = evt.getData(DataProviders.Terminal) ?: return
terminal.getSelectionModel().setSelection(
Position(y = 1, x = 1),
Position(y = terminal.getDocument().getLineCount(), x = terminal.getTerminalModel().getCols())
)
evt.consume()
}
}

View File

@@ -0,0 +1,23 @@
package app.termora.actions
import app.termora.ApplicationScope
import app.termora.Database
import app.termora.TerminalPanelFactory
abstract class TerminalZoomAction : AnAction() {
protected val fontSize get() = Database.getDatabase().terminal.fontSize
abstract fun zoom(): Boolean
override fun actionPerformed(evt: AnActionEvent) {
evt.getData(DataProviders.TerminalPanel) ?: return
if (zoom()) {
ApplicationScope.windowScopes().forEach {
TerminalPanelFactory.getInstance(it)
.fireResize()
}
evt.consume()
}
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.actions
import app.termora.Database
import app.termora.I18n
class TerminalZoomInAction : TerminalZoomAction() {
companion object {
const val ZOOM_IN = "TerminalZoomInAction"
}
init {
putValue(ACTION_COMMAND_KEY, ZOOM_IN)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-in-terminal"))
}
override fun zoom(): Boolean {
Database.getDatabase().terminal.fontSize += 2
return true
}
}

View File

@@ -0,0 +1,22 @@
package app.termora.actions
import app.termora.Database
import app.termora.I18n
import kotlin.math.max
class TerminalZoomOutAction : TerminalZoomAction() {
companion object {
const val ZOOM_OUT = "TerminalZoomOutAction"
}
init {
putValue(ACTION_COMMAND_KEY, ZOOM_OUT)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-out-terminal"))
}
override fun zoom(): Boolean {
val oldFontSize = fontSize
Database.getDatabase().terminal.fontSize = max(fontSize - 2, 9)
return oldFontSize != fontSize
}
}

View File

@@ -0,0 +1,25 @@
package app.termora.actions
import app.termora.Database
import app.termora.I18n
class TerminalZoomResetAction : TerminalZoomAction() {
companion object {
const val ZOOM_RESET = "TerminalZoomResetAction"
}
init {
putValue(ACTION_COMMAND_KEY, ZOOM_RESET)
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-reset-terminal"))
}
private val defaultFontSize = 14
override fun zoom(): Boolean {
if (fontSize == defaultFontSize) {
return false
}
Database.getDatabase().terminal.fontSize = defaultFontSize
return true
}
}

View File

@@ -1,11 +1,14 @@
package app.termora.findeverywhere package app.termora.findeverywhere
import app.termora.* import app.termora.DialogWrapper
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.macro.MacroFindEverywhereProvider import app.termora.macro.MacroFindEverywhereProvider
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatTextField import com.formdev.flatlaf.extras.components.FlatTextField
import com.jetbrains.JBR import com.jetbrains.JBR
import org.jdesktop.swingx.action.ActionManager
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension import java.awt.Dimension
import java.awt.Insets import java.awt.Insets
@@ -20,8 +23,6 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
private val model = DefaultListModel<FindEverywhereResult>() private val model = DefaultListModel<FindEverywhereResult>()
private val resultList = FindEverywhereXList(model) private val resultList = FindEverywhereXList(model)
private val centerPanel = JPanel(BorderLayout()) private val centerPanel = JPanel(BorderLayout())
companion object {
private val providers = mutableListOf<FindEverywhereProvider>( private val providers = mutableListOf<FindEverywhereProvider>(
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()), BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()), BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
@@ -29,15 +30,6 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()), BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
) )
fun registerProvider(provider: FindEverywhereProvider) {
providers.add(provider)
providers.sortBy { it.order() }
}
fun unregisterProvider(provider: FindEverywhereProvider) {
providers.remove(provider)
}
}
init { init {
initView() initView()
@@ -154,7 +146,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
action = action =
if (resultList.selectedIndex + 1 == resultList.elementCount) { if (resultList.selectedIndex + 1 == resultList.elementCount) {
object : AnAction() { object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
resultList.selectedIndex = 1 resultList.selectedIndex = 1
} }
} }
@@ -175,12 +167,12 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
resultList.actionMap.put("action", object : AnAction() { resultList.actionMap.put("action", object : AnAction() {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (resultList.selectedIndex < 0) { if (resultList.selectedIndex < 0) {
return return
} }
val event = ActionEvent(e.source, ActionEvent.ACTION_PERFORMED, String()) val event = ActionEvent(evt.source, ActionEvent.ACTION_PERFORMED, String())
// fire // fire
SwingUtilities.invokeLater { model.get(resultList.selectedIndex).actionPerformed(event) } SwingUtilities.invokeLater { model.get(resultList.selectedIndex).actionPerformed(event) }
@@ -203,22 +195,15 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
} }
}) })
addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
ActionManager.getInstance()
.getAction(Actions.FIND_EVERYWHERE)
.isEnabled = true
} }
override fun windowOpened(e: WindowEvent) { fun registerProvider(provider: FindEverywhereProvider) {
ActionManager.getInstance() providers.add(provider)
.getAction(Actions.FIND_EVERYWHERE) providers.sortBy { it.order() }
.isEnabled = false
} }
}) fun unregisterProvider(provider: FindEverywhereProvider) {
providers.remove(provider)
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {

View File

@@ -0,0 +1,62 @@
package app.termora.findeverywhere
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import org.apache.commons.lang3.StringUtils
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
class FindEverywhereAction : AnAction(StringUtils.EMPTY, Icons.find) {
companion object {
/**
* 查找
*/
const val FIND_EVERYWHERE = "FindEverywhereAction"
}
init {
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-find-everywhere"))
putValue(ACTION_COMMAND_KEY, FIND_EVERYWHERE)
}
override fun actionPerformed(evt: AnActionEvent) {
val scope = evt.getData(DataProviders.WindowScope) ?: return
if (scope.getBoolean("FindEverywhereShown", false)) {
return
}
val source = evt.source
if (source !is Component) {
return
}
val owner = evt.window
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
if (owner != focusedWindow) {
return
}
val dialog = FindEverywhere(owner)
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
dialog.registerProvider(provider)
}
dialog.setLocationRelativeTo(owner)
dialog.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent) {
scope.putBoolean("FindEverywhereShown", false)
}
})
dialog.isVisible = true
scope.putBoolean("FindEverywhereShown", true)
}
}

View File

@@ -1,7 +1,21 @@
package app.termora.findeverywhere package app.termora.findeverywhere
import app.termora.Scope
interface FindEverywhereProvider { interface FindEverywhereProvider {
companion object {
@Suppress("UNCHECKED_CAST")
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
var list = scope.getAnyOrNull("FindEverywhereProviders")
if (list == null) {
list = mutableListOf<FindEverywhereProvider>()
scope.putAny("FindEverywhereProviders", list)
}
return list as MutableList<FindEverywhereProvider>
}
}
/** /**
* 搜索 * 搜索
*/ */

View File

@@ -2,6 +2,7 @@ package app.termora.findeverywhere
import app.termora.Actions import app.termora.Actions
import app.termora.I18n import app.termora.I18n
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
class QuickActionsFindEverywhereProvider : FindEverywhereProvider { class QuickActionsFindEverywhereProvider : FindEverywhereProvider {

View File

@@ -1,9 +1,12 @@
package app.termora.findeverywhere package app.termora.findeverywhere
import app.termora.* import app.termora.Actions
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.NewHostAction
import app.termora.actions.OpenLocalTerminalAction
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import java.awt.event.ActionEvent
import javax.swing.Icon import javax.swing.Icon
class QuickCommandFindEverywhereProvider : FindEverywhereProvider { class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
@@ -11,26 +14,12 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
override fun find(pattern: String): List<FindEverywhereResult> { override fun find(pattern: String): List<FindEverywhereResult> {
val list = mutableListOf<FindEverywhereResult>() val list = mutableListOf<FindEverywhereResult>()
actionManager?.let { actionManager.let { list.add(CreateHostFindEverywhereResult()) }
list.add(CreateHostFindEverywhereResult())
}
// Local terminal // Local terminal
list.add(ActionFindEverywhereResult(object : AnAction( actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
I18n.getString("termora.find-everywhere.quick-command.local-terminal"), list.add(ActionFindEverywhereResult(it))
Icons.terminal
) {
override fun actionPerformed(evt: ActionEvent) {
actionManager.getAction(Actions.OPEN_HOST)?.actionPerformed(
OpenHostActionEvent(
this, Host(
name = name,
protocol = Protocol.Local
)
)
)
} }
}))
// SFTP // SFTP
actionManager.getAction(Actions.SFTP)?.let { actionManager.getAction(Actions.SFTP)?.let {
@@ -50,7 +39,7 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
} }
private class CreateHostFindEverywhereResult : ActionFindEverywhereResult( private class CreateHostFindEverywhereResult : ActionFindEverywhereResult(
ActionManager.getInstance().getAction(Actions.ADD_HOST) ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
) { ) {
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
if (isSelected) { if (isSelected) {

View File

@@ -1,5 +1,6 @@
package app.termora.highlight package app.termora.highlight
import app.termora.ApplicationScope
import app.termora.DialogWrapper import app.termora.DialogWrapper
import app.termora.TerminalFactory import app.termora.TerminalFactory
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
@@ -30,7 +31,8 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {
val panel = JPanel(GridLayout(2, 8, 4, 4)) val panel = JPanel(GridLayout(2, 8, 4, 4))
val colorPalette = TerminalFactory.instance.createTerminal().getTerminalModel().getColorPalette() val colorPalette = TerminalFactory.getInstance(ApplicationScope.forWindowScope(this))
.createTerminal().getTerminalModel().getColorPalette()
for (i in 1..16) { for (i in 1..16) {
val c = JPanel() val c = JPanel()
c.preferredSize = Dimension(24, 24) c.preferredSize = Dimension(24, 24)

View File

@@ -0,0 +1,18 @@
package app.termora.highlight
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
class KeywordHighlightAction : AnAction(
I18n.getString("termora.highlight"),
Icons.edit
) {
override fun actionPerformed(evt: AnActionEvent) {
val owner = evt.window
val dialog = KeywordHighlightDialog(owner)
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
}
}

View File

@@ -19,8 +19,11 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
private val model = KeywordHighlightTableModel() private val model = KeywordHighlightTableModel()
private val table = FlatTable() private val table = FlatTable()
private val keywordHighlightManager by lazy { KeywordHighlightManager.instance } private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
private val colorPalette by lazy { TerminalFactory.instance.createTerminal().getTerminalModel().getColorPalette() } private val colorPalette by lazy {
TerminalFactory.getInstance(ApplicationScope.forWindowScope(this)).createTerminal().getTerminalModel()
.getColorPalette()
}
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add")) private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit")) private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))

View File

@@ -1,17 +1,22 @@
package app.termora.highlight package app.termora.highlight
import app.termora.ApplicationScope
import app.termora.TerminalPanelFactory import app.termora.TerminalPanelFactory
import app.termora.db.Database import app.termora.Database
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class KeywordHighlightManager private constructor() { class KeywordHighlightManager private constructor() {
companion object { companion object {
val instance by lazy { KeywordHighlightManager() } fun getInstance(): KeywordHighlightManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeywordHighlightManager::class) { KeywordHighlightManager() }
}
private val log = LoggerFactory.getLogger(KeywordHighlightManager::class.java) private val log = LoggerFactory.getLogger(KeywordHighlightManager::class.java)
} }
private val database by lazy { Database.instance } private val database by lazy { Database.getDatabase() }
private val keywordHighlights = mutableMapOf<String, KeywordHighlight>() private val keywordHighlights = mutableMapOf<String, KeywordHighlight>()
init { init {
@@ -22,7 +27,7 @@ class KeywordHighlightManager private constructor() {
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) { fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
database.addKeywordHighlight(keywordHighlight) database.addKeywordHighlight(keywordHighlight)
keywordHighlights[keywordHighlight.id] = keywordHighlight keywordHighlights[keywordHighlight.id] = keywordHighlight
TerminalPanelFactory.instance.repaintAll() ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Keyword highlighter added. {}", keywordHighlight) log.debug("Keyword highlighter added. {}", keywordHighlight)
@@ -32,7 +37,7 @@ class KeywordHighlightManager private constructor() {
fun removeKeywordHighlight(id: String) { fun removeKeywordHighlight(id: String) {
database.removeKeywordHighlight(id) database.removeKeywordHighlight(id)
keywordHighlights.remove(id) keywordHighlights.remove(id)
TerminalPanelFactory.instance.repaintAll() ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Keyword highlighter removed. {}", id) log.debug("Keyword highlighter removed. {}", id)

View File

@@ -1,5 +1,6 @@
package app.termora.highlight package app.termora.highlight
import app.termora.ApplicationScope
import app.termora.terminal.* import app.termora.terminal.*
import app.termora.terminal.panel.TerminalDisplay import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener import app.termora.terminal.panel.TerminalPaintListener
@@ -11,11 +12,15 @@ import kotlin.random.Random
class KeywordHighlightPaintListener private constructor() : TerminalPaintListener { class KeywordHighlightPaintListener private constructor() : TerminalPaintListener {
companion object { companion object {
val instance by lazy { KeywordHighlightPaintListener() } fun getInstance(): KeywordHighlightPaintListener {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeywordHighlightPaintListener::class) { KeywordHighlightPaintListener() }
}
private val tag = Random.nextInt() private val tag = Random.nextInt()
} }
private val keywordHighlightManager by lazy { KeywordHighlightManager.instance } private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
override fun before( override fun before(
offset: Int, offset: Int,

View File

@@ -3,7 +3,7 @@ package app.termora.highlight
import javax.swing.table.DefaultTableModel import javax.swing.table.DefaultTableModel
class KeywordHighlightTableModel : DefaultTableModel() { class KeywordHighlightTableModel : DefaultTableModel() {
private val rows get() = KeywordHighlightManager.instance.getKeywordHighlights() private val rows get() = KeywordHighlightManager.getInstance().getKeywordHighlights()
override fun isCellEditable(row: Int, column: Int): Boolean { override fun isCellEditable(row: Int, column: Int): Boolean {
return false return false

View File

@@ -4,7 +4,7 @@ import app.termora.DialogWrapper
import app.termora.DynamicColor import app.termora.DynamicColor
import app.termora.I18n import app.termora.I18n
import app.termora.Icons import app.termora.Icons
import app.termora.db.Database import app.termora.Database
import app.termora.terminal.ColorPalette import app.termora.terminal.ColorPalette
import app.termora.terminal.TerminalColor import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
@@ -29,7 +29,7 @@ class NewKeywordHighlightDialog(
val colorPalette: ColorPalette val colorPalette: ColorPalette
) : DialogWrapper(owner) { ) : DialogWrapper(owner) {
private val formMargin = "7dlu" private val formMargin = "7dlu"
private val keywordHighlightView by lazy { KeywordHighlightView(fontSize = Database.instance.terminal.fontSize) } private val keywordHighlightView by lazy { KeywordHighlightView(fontSize = Database.getDatabase().terminal.fontSize) }
val keywordTextField = FlatTextField() val keywordTextField = FlatTextField()
val descriptionTextField = FlatTextField() val descriptionTextField = FlatTextField()

View File

@@ -0,0 +1,22 @@
package app.termora.keymap
import javax.swing.KeyStroke
class KeyShortcut(val keyStroke: KeyStroke) : Shortcut() {
override fun isKeyboard(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as KeyShortcut
return keyStroke == other.keyStroke
}
override fun hashCode(): Int {
return keyStroke.hashCode()
}
}

View File

@@ -0,0 +1,87 @@
package app.termora.keymap
import app.termora.Application.ohMyJson
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
open class Keymap(
val name: String,
/**
* 当 [getShortcut] [getActionIds] 获取不到的时候从父里面获取
*/
private val parent: Keymap?,
val isReadonly: Boolean = false,
) {
private val shortcuts = mutableMapOf<Shortcut, MutableList<String>>()
open fun addShortcut(actionId: String, shortcut: Shortcut) {
val actionIds = shortcuts.getOrPut(shortcut) { mutableListOf() }
actionIds.removeIf { it == actionId }
actionIds.add(actionId)
}
open fun removeAllActionShortcuts(actionId: Any) {
val iterator = shortcuts.iterator()
while (iterator.hasNext()) {
val shortcut = iterator.next()
shortcut.value.removeIf { it == actionId }
if (shortcut.value.isEmpty()) {
iterator.remove()
}
}
}
open fun getShortcut(actionId: Any): List<Shortcut> {
val shortcuts = mutableListOf<Shortcut>()
for (e in this.shortcuts.entries) {
if (e.value.contains(actionId)) {
shortcuts.add(e.key)
}
}
if (shortcuts.isEmpty()) {
parent?.getShortcut(actionId)?.let { shortcuts.addAll(it) }
}
return shortcuts
}
open fun getShortcuts(): Map<Shortcut, List<String>> {
val shortcuts = mutableMapOf<Shortcut, List<String>>()
shortcuts.putAll(this.shortcuts)
parent?.let { shortcuts.putAll(it.getShortcuts()) }
return shortcuts
}
open fun getActionIds(shortcut: Shortcut): List<String> {
val actionIds = mutableListOf<String>()
shortcuts[shortcut]?.let { actionIds.addAll(it) }
if (actionIds.isEmpty()) {
parent?.getActionIds(shortcut)?.let { actionIds.addAll(it) }
}
return actionIds
}
fun toJSON(): String {
return ohMyJson.encodeToString(buildJsonObject {
put("name", name)
put("readonly", isReadonly)
parent?.let { put("parent", it.name) }
put("shortcuts", buildJsonArray {
for (entry in shortcuts.entries) {
add(buildJsonObject {
put("keyboard", entry.key.isKeyboard())
if (entry.key is KeyShortcut) {
put("keyStroke", (entry.key as KeyShortcut).keyStroke.toString())
}
put("actionIds", ohMyJson.encodeToJsonElement(entry.value))
})
}
})
})
}
}

View File

@@ -0,0 +1,83 @@
package app.termora.keymap
import app.termora.actions.*
import app.termora.findeverywhere.FindEverywhereAction
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
class KeymapImpl(private val menuShortcutKeyMaskEx: Int) : Keymap("Keymap", null, true) {
init {
this.registerShortcuts()
}
private fun registerShortcuts() {
// new window
addShortcut(
NewWindowAction.NEW_WINDOW,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_N, menuShortcutKeyMaskEx))
)
// Find Everywhere
addShortcut(
FindEverywhereAction.FIND_EVERYWHERE,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_T, menuShortcutKeyMaskEx))
)
// Command + L
addShortcut(
OpenLocalTerminalAction.LOCAL_TERMINAL,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_L, menuShortcutKeyMaskEx))
)
// Command + L
addShortcut(
TerminalFindAction.FIND,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_F, menuShortcutKeyMaskEx))
)
// Command + W
addShortcut(
TerminalCloseAction.CLOSE,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_W, menuShortcutKeyMaskEx))
)
// Command + Shift + L
addShortcut(
TerminalClearScreenAction.CLEAR_SCREEN,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_L, menuShortcutKeyMaskEx or InputEvent.SHIFT_DOWN_MASK))
)
// Command + +
addShortcut(
TerminalZoomInAction.ZOOM_IN,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, menuShortcutKeyMaskEx))
)
// Command + -
addShortcut(
TerminalZoomOutAction.ZOOM_OUT,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, menuShortcutKeyMaskEx))
)
// Command + 0
addShortcut(
TerminalZoomResetAction.ZOOM_RESET,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_0, menuShortcutKeyMaskEx))
)
// switch map
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
addShortcut(
SwitchTabAction.SWITCH_TAB,
KeyShortcut(KeyStroke.getKeyStroke(i, menuShortcutKeyMaskEx))
)
}
}
}

View File

@@ -0,0 +1,169 @@
package app.termora.keymap
import app.termora.ApplicationScope
import app.termora.Database
import app.termora.DialogWrapper
import app.termora.Disposable
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.findeverywhere.FindEverywhereAction
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import org.slf4j.LoggerFactory
import java.awt.KeyEventDispatcher
import java.awt.KeyEventPostProcessor
import java.awt.KeyboardFocusManager
import java.awt.event.KeyEvent
import javax.swing.JDialog
import javax.swing.KeyStroke
class KeymapManager private constructor() : Disposable {
companion object {
private val log = LoggerFactory.getLogger(KeymapManager::class.java)
const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
fun getInstance(): KeymapManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(KeymapManager::class) { KeymapManager() }
}
}
private val myKeyEventPostProcessor = MyKeyEventPostProcessor()
private val myKeyEventDispatcher = MyKeyEventDispatcher()
private val database get() = Database.getDatabase()
private val keymaps = linkedMapOf<String, Keymap>()
private val activeKeymap get() = database.properties.getString("Keymap.Active")
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
init {
keyboardFocusManager.addKeyEventPostProcessor(myKeyEventPostProcessor)
keyboardFocusManager.addKeyEventDispatcher(myKeyEventDispatcher)
try {
for (keymap in database.getKeymaps()) {
keymaps[keymap.name] = keymap
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
MacOSKeymap.getInstance().let {
keymaps[it.name] = it
}
WindowsKeymap.getInstance().let {
keymaps[it.name] = it
}
}
fun getActiveKeymap(): Keymap {
val name = activeKeymap
if (name != null) {
val keymap = getKeymap(name)
if (keymap != null) {
return keymap
}
}
return if (SystemInfo.isMacOS) {
MacOSKeymap.getInstance()
} else {
WindowsKeymap.getInstance()
}
}
fun getKeymap(name: String): Keymap? {
return keymaps[name]
}
fun getKeymaps(): List<Keymap> {
return keymaps.values.toList()
}
fun addKeymap(keymap: Keymap) {
keymaps.putFirst(keymap.name, keymap)
database.addKeymap(keymap)
}
fun removeKeymap(name: String) {
keymaps.remove(name)
database.removeKeymap(name)
}
private inner class MyKeyEventPostProcessor : KeyEventPostProcessor {
override fun postProcessKeyEvent(e: KeyEvent): Boolean {
// 只处理 PRESSED 和 带有 modifiers 键的事件
if (!e.isConsumed && e.id == KeyEvent.KEY_PRESSED && e.modifiersEx != 0) {
val shortcuts = getActiveKeymap()
val actionIds = shortcuts.getActionIds(KeyShortcut(KeyStroke.getKeyStrokeForEvent(e)))
if (actionIds.isEmpty()) {
return false
}
val focusedWindow = keyboardFocusManager.focusedWindow
if (focusedWindow is DialogWrapper) {
if (!focusedWindow.processGlobalKeymap) {
return false
}
} else if (focusedWindow is JDialog) {
return false
}
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
for (actionId in actionIds) {
val action = ActionManager.getInstance().getAction(actionId) ?: continue
if (!action.isEnabled) {
continue
}
action.actionPerformed(evt)
if (evt.isConsumed) {
return true
}
}
}
return false
}
}
private inner class MyKeyEventDispatcher : KeyEventDispatcher {
// double shift
private var lastTime = -1L
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
val owner = AnActionEvent(e.source, StringUtils.EMPTY, e).getData(DataProviders.TermoraFrame)
?: return false
if (keyboardFocusManager.focusedWindow == owner) {
val now = System.currentTimeMillis()
if (now - 250 < lastTime) {
app.termora.actions.ActionManager.getInstance()
.getAction(FindEverywhereAction.FIND_EVERYWHERE)
?.actionPerformed(AnActionEvent(e.source, StringUtils.EMPTY, e))
}
lastTime = now
}
} else if (e.keyCode != KeyEvent.VK_SHIFT) { // 如果不是 Shift 键,那么就阻断了连续性,重置时间
lastTime = -1
}
return false
}
}
override fun dispose() {
keyboardFocusManager.removeKeyEventPostProcessor(myKeyEventPostProcessor)
keyboardFocusManager.removeKeyEventDispatcher(myKeyEventDispatcher)
}
}

View File

@@ -0,0 +1,262 @@
package app.termora.keymap
import app.termora.*
import app.termora.actions.ActionManager
import app.termora.actions.SwitchTabAction
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatToolBar
import java.awt.BorderLayout
import java.awt.event.InputEvent
import java.awt.event.ItemEvent
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import javax.swing.*
import javax.swing.table.DefaultTableCellRenderer
class KeymapPanel : JPanel(BorderLayout()) {
private val model = KeymapTableModel()
private val table = JTable(model)
private val keymapManager get() = KeymapManager.getInstance()
private val keymapModel = DefaultComboBoxModel<String>()
private val keymapComboBox = JComboBox(keymapModel)
private val copyBtn = JButton(Icons.copy)
private val renameBtn = JButton(Icons.edit)
private val deleteBtn = JButton(Icons.delete)
private val database get() = Database.getDatabase()
private val allowKeyCodes = mutableSetOf<Int>()
init {
initView()
initEvents()
// select active
keymapComboBox.selectedItem = null
keymapComboBox.selectedItem = keymapManager.getActiveKeymap().name
}
private fun initView() {
for (i in KeyEvent.VK_0..KeyEvent.VK_Z) {
allowKeyCodes.add(i)
}
allowKeyCodes.add(KeyEvent.VK_EQUALS)
allowKeyCodes.add(KeyEvent.VK_MINUS)
copyBtn.toolTipText = I18n.getString("termora.welcome.contextmenu.copy")
renameBtn.toolTipText = I18n.getString("termora.welcome.contextmenu.rename")
deleteBtn.toolTipText = I18n.getString("termora.remove")
table.selectionModel.selectionMode = ListSelectionModel.SINGLE_SELECTION
table.setDefaultRenderer(
Any::class.java,
DefaultTableCellRenderer().apply {
horizontalAlignment = SwingConstants.CENTER
}
)
table.putClientProperty(
FlatClientProperties.STYLE, mapOf(
"showHorizontalLines" to true,
"showVerticalLines" to true,
)
)
val scrollPane = JScrollPane(table)
scrollPane.border = BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
table.background = UIManager.getColor("window")
for (keymap in keymapManager.getKeymaps()) {
keymapModel.addElement(keymap.name)
}
val box = FlatToolBar()
box.add(keymapComboBox)
box.add(Box.createHorizontalStrut(2))
box.add(copyBtn)
box.add(renameBtn)
box.add(deleteBtn)
box.add(Box.createHorizontalGlue())
box.border = BorderFactory.createEmptyBorder(0, 0, 6, 0)
add(box, BorderLayout.NORTH)
add(scrollPane, BorderLayout.CENTER)
}
private fun initEvents() {
table.addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
val row = table.selectedRow
if (row < 0) return
recordKeyShortcut(row, e)
}
})
copyBtn.addActionListener {
val keymap = getCurrentKeymap()
if (keymap != null) {
copyKeymap(keymap)
}
}
keymapComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED && keymapComboBox.selectedItem != null) {
deleteBtn.isEnabled = !(getCurrentKeymap()?.isReadonly ?: true)
renameBtn.isEnabled = deleteBtn.isEnabled
database.properties.putString("Keymap.Active", keymapComboBox.selectedItem as String)
model.fireTableDataChanged()
}
}
renameBtn.addActionListener {
val keymap = getCurrentKeymap()
val index = keymapComboBox.selectedIndex
if (keymap != null && !keymap.isReadonly && index >= 0) {
val text = InputDialog(
SwingUtilities.getWindowAncestor(this@KeymapPanel),
title = renameBtn.toolTipText, text = keymap.name
).getText()
if (!text.isNullOrBlank()) {
if (text != keymap.name) {
keymapManager.removeKeymap(keymap.name)
val newKeymap = cloneKeymap(text, keymap)
keymapManager.addKeymap(newKeymap)
keymapModel.removeElementAt(index)
keymapModel.insertElementAt(text, index)
keymapModel.selectedItem = newKeymap.name
}
}
}
}
deleteBtn.addActionListener {
val keymap = getCurrentKeymap()
val index = keymapComboBox.selectedIndex
if (keymap != null && !keymap.isReadonly && index >= 0) {
if (OptionPane.showConfirmDialog(
SwingUtilities.getWindowAncestor(this),
I18n.getString("termora.keymgr.delete-warning"),
messageType = JOptionPane.WARNING_MESSAGE
) == JOptionPane.YES_OPTION
) {
keymapManager.removeKeymap(keymap.name)
keymapModel.removeElementAt(index)
}
}
}
}
private fun copyKeymap(keymap: Keymap) {
var name = keymap.name + " Copy"
for (i in 0 until Int.MAX_VALUE) {
if (keymapManager.getKeymap(name) == null) {
break
}
name = keymap.name + " Copy(${i + 1})"
}
keymapManager.addKeymap(cloneKeymap(name, keymap))
keymapModel.insertElementAt(name, 0)
keymapComboBox.selectedItem = name
}
private fun cloneKeymap(name: String, keymap: Keymap): Keymap {
val newKeymap = Keymap(name, null, false)
for (e in keymap.getShortcuts()) {
for (actionId in e.value) {
newKeymap.addShortcut(actionId, e.key)
}
}
return newKeymap
}
private fun getCurrentKeymap(): Keymap? {
return keymapManager.getKeymap(keymapComboBox.selectedItem as String)
}
private fun recordKeyShortcut(row: Int, e: KeyEvent) {
val action = model.getAction(row) ?: return
val actionId = (action.getValue(Action.ACTION_COMMAND_KEY) ?: return).toString()
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
// 如果是选择Tab
if (actionId == SwitchTabAction.SWITCH_TAB && keyStroke.keyCode != KeyEvent.VK_BACK_SPACE) {
// 如果是 Tab ,那么 keyCode 必须是功能键
if (keyStroke.keyCode != KeyEvent.VK_META
&& keyStroke.keyCode != KeyEvent.VK_SHIFT
&& keyStroke.keyCode != KeyEvent.VK_CONTROL
&& keyStroke.keyCode != KeyEvent.VK_ALT
) {
return
}
} else if (!isCombinationKey(keyStroke) && keyStroke.keyCode == KeyEvent.VK_BACK_SPACE) {
// ignore
} else if (!isCombinationKey(keyStroke) || (!allowKeyCodes.contains(keyStroke.keyCode))) {
return
}
var keymap = getCurrentKeymap() ?: return
if (keymap.isReadonly) {
copyKeymap(keymap)
keymap = getCurrentKeymap() ?: return
}
e.consume()
val keyShortcut = KeyShortcut(keyStroke)
if (e.keyCode == KeyEvent.VK_BACK_SPACE) {
keymap.removeAllActionShortcuts(actionId)
} else {
val actionIds = keymap.getActionIds(keyShortcut).toMutableList()
actionIds.removeIf { it == actionId }
if (actionIds.isNotEmpty()) {
for (id in actionIds) {
val duplicateAction = ActionManager.getInstance().getAction(id) ?: continue
val text = duplicateAction.getValue(Action.SHORT_DESCRIPTION) ?: continue
OptionPane.showMessageDialog(
SwingUtilities.getWindowAncestor(this@KeymapPanel),
I18n.getString("termora.settings.keymap.already-exists", model.toHumanText(keyStroke), text),
messageType = JOptionPane.ERROR_MESSAGE,
)
}
return
}
keymap.removeAllActionShortcuts(actionId)
// SwitchTab 比较特殊
if (actionId == SwitchTabAction.SWITCH_TAB) {
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
keymap.addShortcut(actionId, KeyShortcut(KeyStroke.getKeyStroke(i, keyStroke.modifiers)))
}
} else {
// 添加到快捷键
keymap.addShortcut(actionId, keyShortcut)
}
}
model.fireTableRowsUpdated(row, row)
keymapManager.addKeymap(keymap)
}
private fun isCombinationKey(keyStroke: KeyStroke): Boolean {
val modifiers = keyStroke.modifiers
return (modifiers and InputEvent.CTRL_DOWN_MASK) != 0
|| (modifiers and InputEvent.SHIFT_DOWN_MASK) != 0
|| (modifiers and InputEvent.ALT_DOWN_MASK) != 0
|| (modifiers and InputEvent.META_DOWN_MASK) != 0
|| (modifiers and InputEvent.ALT_GRAPH_DOWN_MASK) != 0
}
}

View File

@@ -0,0 +1,98 @@
package app.termora.keymap
import app.termora.I18n
import app.termora.actions.*
import app.termora.findeverywhere.FindEverywhereAction
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionManager
import org.jdesktop.swingx.action.BoundAction.ACTION_COMMAND_KEY
import java.awt.event.KeyEvent
import javax.swing.Action
import javax.swing.KeyStroke
import javax.swing.table.DefaultTableModel
class KeymapTableModel : DefaultTableModel() {
private val actionManager get() = ActionManager.getInstance()
private val keymapManager get() = KeymapManager.getInstance()
init {
for (id in listOf(
TerminalCopyAction.COPY,
TerminalPasteAction.PASTE,
TerminalSelectAllAction.SELECT_ALL,
TerminalFindAction.FIND,
TerminalCloseAction.CLOSE,
TerminalZoomInAction.ZOOM_IN,
TerminalZoomOutAction.ZOOM_OUT,
TerminalZoomResetAction.ZOOM_RESET,
OpenLocalTerminalAction.LOCAL_TERMINAL,
FindEverywhereAction.FIND_EVERYWHERE,
NewWindowAction.NEW_WINDOW,
SwitchTabAction.SWITCH_TAB,
)) {
val action = actionManager.getAction(id) ?: continue
super.addRow(arrayOf(action))
}
}
override fun getColumnCount(): Int {
return 2
}
override fun getColumnName(column: Int): String {
return if (column == 0) I18n.getString("termora.settings.keymap.shortcut")
else I18n.getString("termora.settings.keymap.action")
}
fun getAction(row: Int): Action? {
return super.getValueAt(row, 0) as Action?
}
override fun getValueAt(row: Int, column: Int): Any {
val action = getAction(row) ?: return StringUtils.EMPTY
if (column == 0) {
val actionId = action.getValue(ACTION_COMMAND_KEY) ?: StringUtils.EMPTY
val shortcuts = keymapManager.getActiveKeymap().getShortcut(actionId)
if (shortcuts.isNotEmpty()) {
val keyShortcut = shortcuts.first()
if (keyShortcut is KeyShortcut) {
if (actionId == SwitchTabAction.SWITCH_TAB) {
return toHumanText(keyShortcut.keyStroke) + " .. 9"
}
return toHumanText(keyShortcut.keyStroke)
}
}
} else if (column == 1) {
return action.getValue(Action.SHORT_DESCRIPTION)
?: action.getValue(Action.NAME) ?: StringUtils.EMPTY
}
return StringUtils.EMPTY
}
override fun isCellEditable(row: Int, column: Int): Boolean {
return false
}
fun toHumanText(keyStroke: KeyStroke): String {
var text = keyStroke.toString()
text = text.replace("shift", "")
text = text.replace("ctrl", "")
text = text.replace("meta", "")
text = text.replace("alt", "")
text = text.replace("pressed", StringUtils.EMPTY)
text = text.replace(StringUtils.SPACE, StringUtils.EMPTY)
if (keyStroke.keyCode == KeyEvent.VK_EQUALS) {
text = text.replace("EQUALS", "+")
} else if (keyStroke.keyCode == KeyEvent.VK_MINUS) {
text = text.replace("MINUS", "-")
}
return text.toCharArray().joinToString(" + ")
}
}

View File

@@ -0,0 +1,50 @@
package app.termora.keymap
import app.termora.ApplicationScope
import app.termora.actions.TerminalCopyAction
import app.termora.actions.TerminalPasteAction
import app.termora.actions.TerminalSelectAllAction
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
class MacOSKeymap private constructor() : Keymap("macOS", KeymapImpl(InputEvent.META_DOWN_MASK), true) {
companion object {
fun getInstance(): MacOSKeymap {
return ApplicationScope.forApplicationScope().getOrCreate(MacOSKeymap::class) { MacOSKeymap() }
}
}
init {
// Command + C
super.addShortcut(
TerminalCopyAction.COPY,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.META_DOWN_MASK))
)
// Command + V
super.addShortcut(
TerminalPasteAction.PASTE,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.META_DOWN_MASK))
)
// Command + A
super.addShortcut(
TerminalSelectAllAction.SELECT_ALL,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.META_DOWN_MASK))
)
}
override fun removeAllActionShortcuts(actionId: Any) {
throw UnsupportedOperationException()
}
override fun addShortcut(actionId: String, shortcut: Shortcut) {
throw UnsupportedOperationException()
}
}

View File

@@ -0,0 +1,6 @@
package app.termora.keymap
abstract class Shortcut {
abstract fun isKeyboard(): Boolean
}

View File

@@ -0,0 +1,50 @@
package app.termora.keymap
import app.termora.ApplicationScope
import app.termora.actions.TerminalCopyAction
import app.termora.actions.TerminalPasteAction
import app.termora.actions.TerminalSelectAllAction
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
class WindowsKeymap private constructor() : Keymap("Windows", KeymapImpl(InputEvent.CTRL_DOWN_MASK), true) {
companion object {
fun getInstance(): WindowsKeymap {
return ApplicationScope.forApplicationScope().getOrCreate(WindowsKeymap::class) { WindowsKeymap() }
}
}
init {
// Command + C
super.addShortcut(
TerminalCopyAction.COPY,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK))
)
// Command + V
super.addShortcut(
TerminalPasteAction.PASTE,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK))
)
// Command + A
super.addShortcut(
TerminalSelectAllAction.SELECT_ALL,
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK))
)
}
override fun removeAllActionShortcuts(actionId: Any) {
throw UnsupportedOperationException()
}
override fun addShortcut(actionId: String, shortcut: Shortcut) {
throw UnsupportedOperationException()
}
}

View File

@@ -1,16 +1,17 @@
package app.termora.keymgr package app.termora.keymgr
import app.termora.db.Database import app.termora.ApplicationScope
import org.slf4j.LoggerFactory import app.termora.Database
class KeyManager private constructor() { class KeyManager private constructor() {
companion object { companion object {
private val log = LoggerFactory.getLogger(KeyManager::class.java) fun getInstance(): KeyManager {
val instance by lazy { KeyManager() } return ApplicationScope.forApplicationScope().getOrCreate(KeyManager::class) { KeyManager() }
}
} }
private val keyPairs = mutableSetOf<OhKeyPair>() private val keyPairs = mutableSetOf<OhKeyPair>()
private val database get() = Database.instance private val database get() = Database.getDatabase()
init { init {
keyPairs.addAll(database.getKeyPairs()) keyPairs.addAll(database.getKeyPairs())

View File

@@ -0,0 +1,19 @@
package app.termora.keymgr
import app.termora.I18n
import app.termora.Icons
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
class KeyManagerAction : AnAction(
I18n.getString("termora.keymgr.title"),
Icons.greyKey
) {
override fun actionPerformed(evt: AnActionEvent) {
if (this.isEnabled) {
val dialog = KeyManagerDialog(evt.window)
dialog.setLocationRelativeTo(evt.window)
dialog.isVisible = true
}
}
}

View File

@@ -68,7 +68,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
keyPairTable.model = keyPairTableModel keyPairTable.model = keyPairTableModel
keyPairTable.fillsViewportHeight = true keyPairTable.fillsViewportHeight = true
KeyManager.instance.getOhKeyPairs().forEach { KeyManager.getInstance().getOhKeyPairs().forEach {
keyPairTableModel.addRow(arrayOf(it)) keyPairTableModel.addRow(arrayOf(it))
} }
@@ -102,7 +102,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
dialog.isVisible = true dialog.isVisible = true
if (dialog.ohKeyPair != OhKeyPair.empty) { if (dialog.ohKeyPair != OhKeyPair.empty) {
val keyPair = dialog.ohKeyPair val keyPair = dialog.ohKeyPair
KeyManager.instance.addOhKeyPair(keyPair) KeyManager.getInstance().addOhKeyPair(keyPair)
keyPairTableModel.addRow(arrayOf(keyPair)) keyPairTableModel.addRow(arrayOf(keyPair))
} }
} }
@@ -118,7 +118,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
val rows = keyPairTable.selectedRows.sorted().reversed() val rows = keyPairTable.selectedRows.sorted().reversed()
for (row in rows) { for (row in rows) {
val id = keyPairTableModel.getOhKeyPair(row).id val id = keyPairTableModel.getOhKeyPair(row).id
KeyManager.instance.removeOhKeyPair(id) KeyManager.getInstance().removeOhKeyPair(id)
keyPairTableModel.removeRow(row) keyPairTableModel.removeRow(row)
} }
} }
@@ -129,7 +129,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
val dialog = ImportKeyDialog(SwingUtilities.getWindowAncestor(this)) val dialog = ImportKeyDialog(SwingUtilities.getWindowAncestor(this))
dialog.isVisible = true dialog.isVisible = true
if (dialog.ohKeyPair != OhKeyPair.empty) { if (dialog.ohKeyPair != OhKeyPair.empty) {
KeyManager.instance.addOhKeyPair(dialog.ohKeyPair) KeyManager.getInstance().addOhKeyPair(dialog.ohKeyPair)
keyPairTableModel.addRow(arrayOf(dialog.ohKeyPair)) keyPairTableModel.addRow(arrayOf(dialog.ohKeyPair))
} }
} }
@@ -148,7 +148,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
ohKeyPair = dialog.ohKeyPair ohKeyPair = dialog.ohKeyPair
if (ohKeyPair != OhKeyPair.empty) { if (ohKeyPair != OhKeyPair.empty) {
KeyManager.instance.addOhKeyPair(ohKeyPair) KeyManager.getInstance().addOhKeyPair(ohKeyPair)
keyPairTableModel.setValueAt(ohKeyPair, row, 0) keyPairTableModel.setValueAt(ohKeyPair, row, 0)
keyPairTableModel.fireTableRowsUpdated(row, row) keyPairTableModel.fireTableRowsUpdated(row, row)
} }

View File

@@ -46,7 +46,7 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
override fun loadKeys(session: SessionContext?): Iterable<KeyPair> { override fun loadKeys(session: SessionContext?): Iterable<KeyPair> {
val log = OhKeyPairKeyPairProvider.log val log = OhKeyPairKeyPairProvider.log
val ohKeyPair = KeyManager.instance.getOhKeyPair(id) val ohKeyPair = KeyManager.getInstance().getOhKeyPair(id)
if (ohKeyPair == null) { if (ohKeyPair == null) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error("Oh KeyPair [$id] could not be loaded") log.error("Oh KeyPair [$id] could not be loaded")

View File

@@ -2,10 +2,12 @@ package app.termora.macro
import app.termora.* import app.termora.*
import app.termora.AES.encodeBase64String import app.termora.AES.encodeBase64String
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import org.apache.commons.lang3.time.DateFormatUtils import org.apache.commons.lang3.time.DateFormatUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.event.ActionEvent
import java.util.* import java.util.*
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -24,12 +26,13 @@ class MacroAction : AnAction(I18n.getString("termora.macro"), Icons.rec) {
var isRecording = false var isRecording = false
private set private set
private val macroManager get() = MacroManager.instance private val macroManager get() = MacroManager.getInstance()
private val terminalTabbedManager get() = Application.getService(TerminalTabbedManager::class)
override fun actionPerformed(evt: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
val source = evt.source val source = evt.source
if (source !is JComponent) return if (source !is JComponent) return
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
isSelected = isRecording isSelected = isRecording
@@ -42,6 +45,7 @@ class MacroAction : AnAction(I18n.getString("termora.macro"), Icons.rec) {
val macros = macroManager.getMacros().sortedByDescending { it.sort } val macros = macroManager.getMacros().sortedByDescending { it.sort }
// 播放最后一个 // 播放最后一个
menu.add(MacroPlaybackAction()) menu.add(MacroPlaybackAction())
@@ -50,7 +54,7 @@ class MacroAction : AnAction(I18n.getString("termora.macro"), Icons.rec) {
val count = min(macros.size, 10) val count = min(macros.size, 10)
for (i in 0 until count) { for (i in 0 until count) {
val macro = macros[i] val macro = macros[i]
menu.add(macro.name).addActionListener { runMacro(macro) } menu.add(macro.name).addActionListener { runMacro(windowScope, macro) }
} }
} }
@@ -90,7 +94,8 @@ class MacroAction : AnAction(I18n.getString("termora.macro"), Icons.rec) {
macroManager.addMacro(macro) macroManager.addMacro(macro)
} }
fun runMacro(macro: Macro) { fun runMacro(windowScope: WindowScope, macro: Macro) {
val terminalTabbedManager = windowScope.get(TerminalTabbedManager::class)
val tab = terminalTabbedManager.getSelectedTerminalTab() ?: return val tab = terminalTabbedManager.getSelectedTerminalTab() ?: return
if (tab !is PtyHostTerminalTab) { if (tab !is PtyHostTerminalTab) {

View File

@@ -1,6 +1,10 @@
package app.termora.macro package app.termora.macro
import app.termora.* import app.termora.*
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
@@ -18,7 +22,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
private val model = DefaultListModel<Macro>() private val model = DefaultListModel<Macro>()
private val list = JList(model) private val list = JList(model)
private val macroManager by lazy { MacroManager.instance } private val macroManager by lazy { MacroManager.getInstance() }
private val runBtn = JButton(I18n.getString("termora.macro.run")) private val runBtn = JButton(I18n.getString("termora.macro.run"))
private val editBtn = JButton(I18n.getString("termora.keymgr.edit")) private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
@@ -107,15 +111,18 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
} }
} }
runBtn.addActionListener { runBtn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
val index = list.selectedIndex val index = list.selectedIndex
if (index >= 0) { if (index >= 0) {
val macroAction = ActionManager.getInstance().getAction(Actions.MACRO) val macroAction = ActionManager.getInstance().getAction(Actions.MACRO)
if (macroAction is MacroAction) { if (macroAction is MacroAction) {
macroAction.runMacro(model.getElementAt(index)) macroAction.runMacro(windowScope, model.getElementAt(index))
} }
} }
} }
})
copyBtn.addActionListener { copyBtn.addActionListener {
if (list.selectionModel.selectedItemsCount > 0) { if (list.selectionModel.selectedItemsCount > 0) {

View File

@@ -1,18 +1,20 @@
package app.termora.macro package app.termora.macro
import app.termora.Actions import app.termora.Actions
import app.termora.AnAction import app.termora.ApplicationScope
import app.termora.I18n import app.termora.I18n
import app.termora.actions.AnAction
import app.termora.findeverywhere.ActionFindEverywhereResult import app.termora.findeverywhere.ActionFindEverywhereResult
import app.termora.findeverywhere.FindEverywhereProvider import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereResult import app.termora.findeverywhere.FindEverywhereResult
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import java.awt.Component
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import javax.swing.Icon import javax.swing.Icon
import kotlin.math.min import kotlin.math.min
class MacroFindEverywhereProvider : FindEverywhereProvider { class MacroFindEverywhereProvider : FindEverywhereProvider {
private val macroManager get() = MacroManager.instance private val macroManager get() = MacroManager.getInstance()
override fun find(pattern: String): List<FindEverywhereResult> { override fun find(pattern: String): List<FindEverywhereResult> {
val macroAction = ActionManager.getInstance().getAction(Actions.MACRO) ?: return emptyList() val macroAction = ActionManager.getInstance().getAction(Actions.MACRO) ?: return emptyList()
@@ -62,7 +64,10 @@ class MacroFindEverywhereProvider : FindEverywhereProvider {
private val macroAction: MacroAction private val macroAction: MacroAction
) : FindEverywhereResult { ) : FindEverywhereResult {
override fun actionPerformed(e: ActionEvent) { override fun actionPerformed(e: ActionEvent) {
macroAction.runMacro(macro) val source = e.source
if (source is Component) {
macroAction.runMacro(ApplicationScope.forWindowScope(source), macro)
}
} }
override fun toString(): String { override fun toString(): String {

View File

@@ -1,6 +1,7 @@
package app.termora.macro package app.termora.macro
import app.termora.db.Database import app.termora.ApplicationScope
import app.termora.Database
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/** /**
@@ -8,13 +9,15 @@ import org.slf4j.LoggerFactory
*/ */
class MacroManager private constructor() { class MacroManager private constructor() {
companion object { companion object {
val instance by lazy { MacroManager() } fun getInstance(): MacroManager {
return ApplicationScope.forApplicationScope().getOrCreate(MacroManager::class) { MacroManager() }
}
private val log = LoggerFactory.getLogger(MacroManager::class.java) private val log = LoggerFactory.getLogger(MacroManager::class.java)
} }
private val macros = mutableMapOf<String, Macro>() private val macros = mutableMapOf<String, Macro>()
private val database get() = Database.instance private val database get() = Database.getDatabase()
init { init {
macros.putAll(database.getMacros().associateBy { it.id }) macros.putAll(database.getMacros().associateBy { it.id })

View File

@@ -1,22 +1,24 @@
package app.termora.macro package app.termora.macro
import app.termora.Actions import app.termora.Actions
import app.termora.AnAction
import app.termora.I18n import app.termora.I18n
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager
import java.awt.event.ActionEvent
class MacroPlaybackAction : AnAction( class MacroPlaybackAction : AnAction(
I18n.getString("termora.macro.playback"), I18n.getString("termora.macro.playback"),
) { ) {
private val macroAction get() = ActionManager.getInstance().getAction(Actions.MACRO) as MacroAction? private val macroAction get() = ActionManager.getInstance().getAction(Actions.MACRO) as MacroAction?
private val macroManager get() = MacroManager.instance private val macroManager get() = MacroManager.getInstance()
override fun actionPerformed(evt: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
val macros = macroManager.getMacros().sortedByDescending { it.sort } val macros = macroManager.getMacros().sortedByDescending { it.sort }
if (macros.isEmpty() || macroAction == null) { if (macros.isEmpty() || macroAction == null) {
return return
} }
macroAction?.runMacro(macros.first()) macroAction?.runMacro(evt.getData(DataProviders.WindowScope) ?: return, macros.first())
} }
override fun isEnabled(): Boolean { override fun isEnabled(): Boolean {

View File

@@ -1,6 +1,7 @@
package app.termora.macro package app.termora.macro
import app.termora.Actions import app.termora.Actions
import app.termora.terminal.PtyConnector import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate import app.termora.terminal.PtyConnectorDelegate
import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.action.ActionManager

Some files were not shown because too many files have changed in this diff Show More