mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: 改进事件系统与全局快捷键 (#62)
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将在 JVM 进程退出时释放
|
|
||||||
*/
|
|
||||||
class ApplicationDisposable : Disposable {
|
|
||||||
companion object {
|
|
||||||
val instance by lazy { ApplicationDisposable() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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") }
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/main/kotlin/app/termora/Scope.kt
Normal file
173
src/main/kotlin/app/termora/Scope.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
60
src/main/kotlin/app/termora/TermoraFrameManager.kt
Normal file
60
src/main/kotlin/app/termora/TermoraFrameManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
71
src/main/kotlin/app/termora/actions/ActionManager.kt
Normal file
71
src/main/kotlin/app/termora/actions/ActionManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
src/main/kotlin/app/termora/actions/AnAction.kt
Normal file
30
src/main/kotlin/app/termora/actions/AnAction.kt
Normal 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)
|
||||||
|
|
||||||
|
}
|
||||||
61
src/main/kotlin/app/termora/actions/AnActionEvent.kt
Normal file
61
src/main/kotlin/app/termora/actions/AnActionEvent.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/main/kotlin/app/termora/actions/AppUpdateAction.kt
Normal file
117
src/main/kotlin/app/termora/actions/AppUpdateAction.kt
Normal 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}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/kotlin/app/termora/actions/DataProvider.kt
Normal file
21
src/main/kotlin/app/termora/actions/DataProvider.kt
Normal 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?
|
||||||
|
}
|
||||||
24
src/main/kotlin/app/termora/actions/DataProviderSupport.kt
Normal file
24
src/main/kotlin/app/termora/actions/DataProviderSupport.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/kotlin/app/termora/actions/DataProviders.kt
Normal file
18
src/main/kotlin/app/termora/actions/DataProviders.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/kotlin/app/termora/actions/MultipleAction.kt
Normal file
17
src/main/kotlin/app/termora/actions/MultipleAction.kt
Normal 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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/kotlin/app/termora/actions/NewHostAction.kt
Normal file
46
src/main/kotlin/app/termora/actions/NewHostAction.kt
Normal 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))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/kotlin/app/termora/actions/NewWindowAction.kt
Normal file
27
src/main/kotlin/app/termora/actions/NewWindowAction.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/main/kotlin/app/termora/actions/OpenHostAction.kt
Normal file
28
src/main/kotlin/app/termora/actions/OpenHostAction.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
53
src/main/kotlin/app/termora/actions/SettingsAction.kt
Normal file
53
src/main/kotlin/app/termora/actions/SettingsAction.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/kotlin/app/termora/actions/SwitchTabAction.kt
Normal file
36
src/main/kotlin/app/termora/actions/SwitchTabAction.kt
Normal 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()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
24
src/main/kotlin/app/termora/actions/TerminalCloseAction.kt
Normal file
24
src/main/kotlin/app/termora/actions/TerminalCloseAction.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
23
src/main/kotlin/app/termora/actions/TerminalFindAction.kt
Normal file
23
src/main/kotlin/app/termora/actions/TerminalFindAction.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
35
src/main/kotlin/app/termora/actions/TerminalPasteAction.kt
Normal file
35
src/main/kotlin/app/termora/actions/TerminalPasteAction.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/kotlin/app/termora/actions/TerminalZoomAction.kt
Normal file
23
src/main/kotlin/app/termora/actions/TerminalZoomAction.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/kotlin/app/termora/actions/TerminalZoomInAction.kt
Normal file
20
src/main/kotlin/app/termora/actions/TerminalZoomInAction.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/kotlin/app/termora/actions/TerminalZoomOutAction.kt
Normal file
22
src/main/kotlin/app/termora/actions/TerminalZoomOutAction.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索
|
* 搜索
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
22
src/main/kotlin/app/termora/keymap/KeyShortcut.kt
Normal file
22
src/main/kotlin/app/termora/keymap/KeyShortcut.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/main/kotlin/app/termora/keymap/Keymap.kt
Normal file
87
src/main/kotlin/app/termora/keymap/Keymap.kt
Normal 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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
83
src/main/kotlin/app/termora/keymap/KeymapImpl.kt
Normal file
83
src/main/kotlin/app/termora/keymap/KeymapImpl.kt
Normal 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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/main/kotlin/app/termora/keymap/KeymapManager.kt
Normal file
169
src/main/kotlin/app/termora/keymap/KeymapManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
262
src/main/kotlin/app/termora/keymap/KeymapPanel.kt
Normal file
262
src/main/kotlin/app/termora/keymap/KeymapPanel.kt
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
src/main/kotlin/app/termora/keymap/KeymapTableModel.kt
Normal file
98
src/main/kotlin/app/termora/keymap/KeymapTableModel.kt
Normal 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(" + ")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
50
src/main/kotlin/app/termora/keymap/MacOSKeymap.kt
Normal file
50
src/main/kotlin/app/termora/keymap/MacOSKeymap.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
6
src/main/kotlin/app/termora/keymap/Shortcut.kt
Normal file
6
src/main/kotlin/app/termora/keymap/Shortcut.kt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package app.termora.keymap
|
||||||
|
|
||||||
|
abstract class Shortcut {
|
||||||
|
abstract fun isKeyboard(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
50
src/main/kotlin/app/termora/keymap/WindowsKeymap.kt
Normal file
50
src/main/kotlin/app/termora/keymap/WindowsKeymap.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
19
src/main/kotlin/app/termora/keymgr/KeyManagerAction.kt
Normal file
19
src/main/kotlin/app/termora/keymgr/KeyManagerAction.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user