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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,14 +16,11 @@ import java.awt.Desktop
import java.io.File
import java.net.URI
import java.time.Duration
import java.util.*
import kotlin.math.ln
import kotlin.math.pow
import kotlin.reflect.KClass
object Application {
private val services = Collections.synchronizedMap(mutableMapOf<KClass<*>, Any>())
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) {
if (SystemInfo.isWindows) {
ProcessBuilder("explorer", uri.toString()).start()

View File

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

View File

@@ -1,6 +1,7 @@
package app.termora
import app.termora.db.Database
import app.termora.actions.ActionManager
import app.termora.keymap.KeymapManager
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatSystemProperties
import com.formdev.flatlaf.extras.FlatInspector
@@ -28,8 +29,8 @@ import java.io.RandomAccessFile
import java.nio.channels.FileLock
import java.util.*
import javax.swing.*
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
class ApplicationRunner {
private lateinit var singletonLock: FileLock
@@ -41,39 +42,62 @@ class ApplicationRunner {
}
fun run() {
measureTimeMillis {
// 覆盖 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
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() {
if (Doorman.instance.isWorking()) {
if (Doorman.getInstance().isWorking()) {
if (!DoormanDialog(null).open()) {
exitProcess(1)
}
@@ -81,17 +105,11 @@ class ApplicationRunner {
}
private fun startMainFrame() {
val frame = TermoraFrame()
frame.title = if (SystemInfo.isLinux) null else Application.getName()
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
frame.setSize(1280, 800)
frame.setLocationRelativeTo(null)
frame.isVisible = true
TermoraFrameManager.getInstance().createWindow().isVisible = true
}
private fun loadSettings() {
val language = Database.instance.appearance.language
val language = Database.getDatabase().appearance.language
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
if (log.isInfoEnabled) {
log.info("Language: {} , Locale: {}", language, locale)
@@ -110,10 +128,9 @@ class ApplicationRunner {
JDialog.setDefaultLookAndFeelDecorated(true)
}
val themeManager = ThemeManager.instance
val settings = Database.instance
val themeManager = ThemeManager.getInstance()
val settings = Database.getDatabase()
var theme = settings.appearance.theme
// 如果是跟随系统或者不存在样式,那么使用默认的
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) {
theme = if (OsThemeDetector.getDetector().isDark) {
@@ -125,6 +142,7 @@ class ApplicationRunner {
themeManager.change(theme, true)
if (Application.isUnknownVersion())
FlatInspector.install("ctrl shift alt X");
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
@@ -173,21 +191,21 @@ class ApplicationRunner {
}
private fun printSystemInfo() {
if (log.isInfoEnabled) {
log.info("Welcome to ${Application.getName()} ${Application.getVersion()}!")
log.info(
if (log.isDebugEnabled) {
log.debug("Welcome to ${Application.getName()} ${Application.getVersion()}!")
log.debug(
"JVM name: {} , vendor: {} , version: {}",
SystemUtils.JAVA_VM_NAME,
SystemUtils.JAVA_VM_VENDOR,
SystemUtils.JAVA_VM_VERSION,
)
log.info(
log.debug(
"OS name: {} , version: {} , arch: {}",
SystemUtils.OS_NAME,
SystemUtils.OS_VERSION,
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() {
val dir = Application.getDatabaseFile()
try {
Database.open(dir)
Database.getDatabase()
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
@@ -296,10 +313,10 @@ class ApplicationRunner {
}
private fun getAnalyticsUserID(): String {
var id = Database.instance.properties.getString("AnalyticsUserID")
var id = Database.getDatabase().properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) {
id = UUID.randomUUID().toSimpleString()
Database.instance.properties.putString("AnalyticsUserID", id)
Database.getDatabase().properties.putString("AnalyticsUserID", id)
}
return id
}

View File

@@ -1,7 +1,7 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.db.Database
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.serialization.encodeToString
@@ -358,7 +358,8 @@ class CustomizeToolBarDialog(
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()
}

View File

@@ -1,8 +1,9 @@
package app.termora.db
package app.termora
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.highlight.KeywordHighlight
import app.termora.keymap.KeyShortcut
import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro
import app.termora.sync.SyncType
@@ -13,10 +14,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.util.*
import javax.swing.KeyStroke
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
@@ -26,24 +29,15 @@ import kotlin.time.Duration.Companion.minutes
class Database private constructor(private val env: Environment) : Disposable {
companion object {
private const val KEYMAP_STORE = "Keymap"
private const val HOST_STORE = "Host"
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
private const val MACRO_STORE = "Macro"
private const val KEY_PAIR_STORE = "KeyPair"
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) {
if (::database.isInitialized) {
throw UnsupportedOperationException("Database is already open")
}
private fun open(dir: File): Database {
val config = EnvironmentConfig()
// 32MB
config.setLogFileSize(1024 * 32)
@@ -51,8 +45,12 @@ class Database private constructor(private val env: Environment) : Disposable {
// 5m
config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt())
val environment = Environments.newInstance(dir, config)
database = Database(environment)
Disposer.register(ApplicationDisposable.instance, database)
return Database(environment)
}
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 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> {
@@ -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) {
private val doorman get() = Doorman.instance
private val doorman get() = Doorman.getInstance()
public override fun getString(key: String): String? {
var value = super.getString(key)

View File

@@ -1,5 +1,7 @@
package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.util.SystemInfo
@@ -7,7 +9,6 @@ import com.jetbrains.JBR
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Window
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
@@ -21,6 +22,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
companion object {
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 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() {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
initTitleBar()
@@ -132,7 +146,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
rootPane.actionMap.put("close", object : AnAction() {
override fun actionPerformed(e: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
doCancelAction()
}
})
@@ -154,12 +168,12 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
if (SystemInfo.isWindows) {
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
override fun windowClosed(e: WindowEvent) {
ThemeManager.instance.removeThemeChangeListener(this)
ThemeManager.getInstance().removeThemeChangeListener(this)
}
override fun windowOpened(e: WindowEvent) {
onChanged()
ThemeManager.instance.addThemeChangeListener(this)
ThemeManager.getInstance().addThemeChangeListener(this)
}
override fun onChanged() {
@@ -190,7 +204,8 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
putValue(DEFAULT_ACTION, true)
}
override fun actionPerformed(e: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
doOKAction()
}
@@ -198,7 +213,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) {
override fun actionPerformed(e: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
doCancelAction()
}

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
generalOption.remarkTextArea.text = host.remark
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
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) {
generalOption.publicKeyTextField.text = ohKeyPair.name
generalOption.publicKeyTextField.putClientProperty(OhKeyPair::class, ohKeyPair)

View File

@@ -1,12 +1,13 @@
package app.termora
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.session.ClientSession
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Window
import java.awt.event.ActionEvent
import javax.swing.*
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 {
return object : AnAction(I18n.getString("termora.new-host.test-connection")) {
override fun actionPerformed(e: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
if (!pane.validateFields()) {
return
}

View File

@@ -1,6 +1,5 @@
package app.termora
import app.termora.db.Database
import java.util.*
interface HostListener : EventListener {
@@ -12,10 +11,12 @@ interface HostListener : EventListener {
class HostManager private constructor() {
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>()
fun addHost(host: Host, notify: Boolean = true) {

View File

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

View File

@@ -1,6 +1,8 @@
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.icons.FlatTreeClosedIcon
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
@@ -24,7 +26,7 @@ import javax.swing.tree.TreeSelectionModel
class HostTree : JTree(), Disposable {
private val hostManager get() = HostManager.instance
private val hostManager get() = HostManager.getInstance()
private val editor = OutlineTextField(64)
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) {
TreeUtils.loadExpansionState(this@HostTree, state)
}
@@ -132,8 +134,8 @@ class HostTree : JTree(), Disposable {
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
val host = lastSelectedPathComponent
if (host is Host && host.protocol != Protocol.Folder) {
ActionManager.getInstance().getAction(Actions.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, host))
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
}
}
}
@@ -328,13 +330,13 @@ class HostTree : JTree(), Disposable {
popupMenu.addSeparator()
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
open.addActionListener {
open.addActionListener { evt ->
getSelectionNodes()
.filter { it.protocol != Protocol.Folder }
.forEach {
ActionManager.getInstance()
.getAction(Actions.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(this, it))
.getAction(OpenHostAction.OPEN_HOST)
?.actionPerformed(OpenHostActionEvent(evt.source, it, evt))
}
}
@@ -412,7 +414,8 @@ class HostTree : JTree(), Disposable {
newHost.addActionListener(object : AbstractAction() {
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)
}
fun showAddHostDialog() {
var lastHost = lastSelectedPathComponent
if (lastHost !is Host) {
return
}
if (lastHost.protocol != Protocol.Folder) {
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) {
fun expandNode(node: Host, including: Boolean = false) {
expandPath(TreePath(model.getPathToRoot(node)))
if (including) {
model.getChildren(node).forEach { expandNode(it, true) }
@@ -552,7 +533,7 @@ class HostTree : JTree(), Disposable {
}
override fun dispose() {
Database.instance.properties.putString(
Database.getDatabase().properties.putString(
"HostTreeExpansionState",
TreeUtils.saveExpansionState(this)
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,10 @@ object Icons {
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_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 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 empty by lazy { DynamicIcon("icons/empty.svg") }
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }

View File

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

View File

@@ -1,5 +1,6 @@
package app.termora
import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate
import org.jdesktop.swingx.action.ActionManager
@@ -7,10 +8,15 @@ import org.jdesktop.swingx.action.ActionManager
/**
* 当开启转发时,会获取到所有的 [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 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) {
if (isMultiple) {

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,26 @@
package app.termora
import app.termora.db.Database
import app.termora.macro.MacroPtyConnector
import app.termora.terminal.PtyConnector
import app.termora.terminal.PtyConnectorDelegate
import app.termora.terminal.PtyProcessConnector
import com.pty4j.PtyProcessBuilder
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils
import org.slf4j.LoggerFactory
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.*
class PtyConnectorFactory {
class PtyConnectorFactory : Disposable {
private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>())
private val database get() = Database.instance
private val database get() = Database.getDatabase()
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(
@@ -29,12 +33,25 @@ class PtyConnectorFactory {
envs["TERM"] = "xterm-256color"
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 commands = mutableListOf(command)
if (SystemUtils.IS_OS_UNIX) {
commands.add("-l")
}
if (log.isDebugEnabled) {
log.debug("command: {} , envs: {}", commands.joinToString(" "), envs)
}
val ptyProcess = PtyProcessBuilder(commands.toTypedArray())
.setEnvironment(envs)
.setInitialRows(rows)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,10 @@ package app.termora
import app.termora.AES.encodeBase64String
import app.termora.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.keymap.KeymapPanel
import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager
import app.termora.native.FileChooser
@@ -40,7 +42,6 @@ import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.Component
import java.awt.datatransfer.StringSelection
import java.awt.event.ActionEvent
import java.awt.event.ItemEvent
import java.io.File
import java.net.URI
@@ -53,7 +54,7 @@ import kotlin.time.Duration.Companion.milliseconds
class SettingsOptionsPane : OptionsPane() {
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
private val database get() = Database.instance
private val database get() = Database.getDatabase()
companion object {
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
@@ -96,6 +97,7 @@ class SettingsOptionsPane : OptionsPane() {
init {
addOption(AppearanceOption())
addOption(TerminalOption())
addOption(KeyShortcutsOption())
addOption(CloudSyncOption())
addOption(DoormanOption())
addOption(AboutOption())
@@ -103,7 +105,7 @@ class SettingsOptionsPane : OptionsPane() {
}
private inner class AppearanceOption : JPanel(BorderLayout()), Option {
val themeManager = ThemeManager.instance
val themeManager = ThemeManager.getInstance()
val themeComboBox = FlatComboBox<String>()
val languageComboBox = FlatComboBox<String>()
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(languageComboBox).xy(3, rows)
.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"))
}
})).xy(5, rows).apply { rows += step }
@@ -234,7 +236,7 @@ class SettingsOptionsPane : OptionsPane() {
private val shellComboBox = FlatComboBox<String>()
private val maxRowsTextField = IntSpinner(0, 0)
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()
init {
@@ -270,7 +272,7 @@ class SettingsOptionsPane : OptionsPane() {
if (it.stateChange == ItemEvent.SELECTED) {
val style = cursorStyleComboBox.selectedItem as CursorStyle
terminalSetting.cursor = style
TerminalFactory.instance.getTerminals().forEach { e ->
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
e.getTerminalModel().setData(DataKey.CursorStyle, style)
}
}
@@ -280,7 +282,7 @@ class SettingsOptionsPane : OptionsPane() {
debugComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.debug = debugComboBox.selectedItem as Boolean
TerminalFactory.instance.getTerminals().forEach {
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
}
}
@@ -296,7 +298,10 @@ class SettingsOptionsPane : OptionsPane() {
}
private fun fireFontChanged() {
TerminalPanelFactory.instance.fireResize()
ApplicationScope.windowScopes().forEach {
TerminalPanelFactory.getInstance(it)
.fireResize()
}
}
private fun initView() {
@@ -489,7 +494,11 @@ class SettingsOptionsPane : OptionsPane() {
getTokenBtn.addActionListener {
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.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("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
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)) {
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.instance.getOhKeyPairs()))
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.getInstance().getOhKeyPairs()))
}
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
put(
"keywordHighlights",
ohMyJson.encodeToJsonElement(KeywordHighlightManager.instance.getKeywordHighlights())
ohMyJson.encodeToJsonElement(KeywordHighlightManager.getInstance().getKeywordHighlights())
)
}
if (syncConfig.ranges.contains(SyncRange.Macros)) {
put(
"macros",
ohMyJson.encodeToJsonElement(MacroManager.instance.getMacros())
ohMyJson.encodeToJsonElement(MacroManager.getInstance().getMacros())
)
}
put("settings", buildJsonObject {
@@ -670,7 +679,7 @@ class SettingsOptionsPane : OptionsPane() {
// sync
val syncResult = kotlin.runCatching {
val syncer = SyncerProvider.instance.getSyncer(syncConfig.type)
val syncer = SyncerProvider.getInstance().getSyncer(syncConfig.type)
if (push) {
syncer.push(syncConfig)
} else {
@@ -905,10 +914,10 @@ class SettingsOptionsPane : OptionsPane() {
private fun createHyperlink(url: String, text: String = url): Hyperlink {
return Hyperlink(object : AnAction(text) {
override fun actionPerformed(evt: ActionEvent) {
override fun actionPerformed(evt: AnActionEvent) {
Application.browse(URI.create(url))
}
});
})
}
private fun initEvents() {}
@@ -934,9 +943,9 @@ class SettingsOptionsPane : OptionsPane() {
private val twoPasswordTextField = OutlinePasswordField(255)
private val tip = FlatLabel()
private val safeBtn = FlatButton()
private val doorman get() = Doorman.instance
private val hostManager get() = HostManager.instance
private val keyManager get() = KeyManager.instance
private val doorman get() = Doorman.getInstance()
private val hostManager get() = HostManager.getInstance()
private val keyManager get() = KeyManager.getInstance()
init {
initView()
@@ -1159,5 +1168,35 @@ class SettingsOptionsPane : OptionsPane() {
}
private inner class KeyShortcutsOption : JPanel(BorderLayout()), Option {
private val keymapPanel = KeymapPanel()
init {
initView()
initEvents()
}
private fun initView() {
add(keymapPanel, BorderLayout.CENTER)
}
private fun initEvents() {}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.fitContent
}
override fun getTitle(): String {
return I18n.getString("termora.settings.keymap")
}
override fun getJComponent(): JComponent {
return this
}
}
}

View File

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

View File

@@ -13,14 +13,16 @@ class TerminalPanelFactory {
private val terminalPanels = mutableListOf<TerminalPanel>()
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 {
val terminalPanel = TerminalPanel(terminal, ptyConnector)
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.instance)
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.instance)
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
terminalPanels.add(terminalPanel)
return terminalPanel
}

View File

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

View File

@@ -1,28 +1,35 @@
package app.termora
import app.termora.actions.*
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhere
import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereResult
import app.termora.terminal.DataKey
import app.termora.transport.TransportPanel
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import org.jdesktop.swingx.action.ActionManager
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 javax.swing.*
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
import kotlin.math.min
class TerminalTabbed(
private val windowScope: WindowScope,
private val termoraToolBar: TermoraToolBar,
private val tabbedPane: FlatTabbedPane,
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager {
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
private val tabs = mutableListOf<TerminalTab>()
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
private val toolbar = termoraToolBar.getJToolBar()
private val actionManager = ActionManager.getInstance()
private val dataProviderSupport = DataProviderSupport()
private val iconListener = PropertyChangeListener { e ->
val source = e.source
@@ -52,6 +59,10 @@ class TerminalTabbed(
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() {
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> {
val results = mutableListOf<FindEverywhereResult>()
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)
@@ -210,7 +183,9 @@ class TerminalTabbed(
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)
tab.start()
}
@@ -242,32 +217,34 @@ class TerminalTabbed(
// 克隆
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
clone.addActionListener {
clone.addActionListener { evt ->
if (tab is HostTerminalTab) {
ActionManager.getInstance()
.getAction(Actions.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host))
actionManager
.getAction(OpenHostAction.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host, evt))
}
}
// 在新窗口中打开
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
openInNewWindow.addActionListener {
val index = tabbedPane.selectedIndex
if (index > 0) {
val title = tabbedPane.getTitleAt(index)
removeTabAt(index, false)
openInNewWindow.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val owner = evt.getData(DataProviders.TermoraFrame) ?: return
if (tabIndex > 0) {
val title = tabbedPane.getTitleAt(tabIndex)
removeTabAt(tabIndex, false)
val dialog = TerminalTabDialog(
owner = SwingUtilities.getWindowAncestor(this),
owner = owner,
terminalTab = tab,
size = Dimension(min(size.width, 1280), min(size.height, 800))
)
dialog.title = title
Disposer.register(dialog, tab)
Disposer.register(this, dialog)
Disposer.register(this@TerminalTabbed, dialog)
dialog.isVisible = true
}
}
})
popupMenu.addSeparator()
@@ -451,5 +428,18 @@ class TerminalTabbed(
}
}
override fun closeTerminalTab(tab: TerminalTab) {
for (i in 0 until tabs.size) {
if (tabs[i] == tab) {
removeTabAt(i, true)
break
}
}
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
}
}

View File

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

View File

@@ -1,84 +1,53 @@
package app.termora
import app.termora.findeverywhere.FindEverywhere
import app.termora.highlight.KeywordHighlightDialog
import app.termora.keymgr.KeyManagerDialog
import app.termora.macro.MacroAction
import app.termora.tlog.TerminalLoggerAction
import app.termora.transport.SFTPAction
import app.termora.actions.ActionManager
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatDesktop
import com.formdev.flatlaf.util.SystemInfo
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.Insets
import java.awt.KeyEventDispatcher
import java.awt.KeyboardFocusManager
import java.awt.event.*
import java.net.URI
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.*
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.event.HyperlinkEvent
import kotlin.concurrent.fixedRateTimer
import javax.swing.UIManager
import kotlin.math.max
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
fun assertEventDispatchThread() {
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 tabbedPane = MyTabbedPane()
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
private lateinit var terminalTabbed: TerminalTabbed
private val disposable = Disposer.newDisposable()
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
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 {
initActions()
initView()
initEvents()
initDesktopHandler()
scheduleUpdate()
}
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) {
ThemeManager.instance.addThemeChangeListener(object : ThemeChangeListener {
ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
override fun onChanged() {
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() {
if (isWindowDecorationsSupported) {
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
@@ -267,10 +101,7 @@ class TermoraFrame : JFrame() {
}
minimumSize = Dimension(640, 400)
terminalTabbed = TerminalTabbed(toolbar, tabbedPane).apply {
Application.registerService(TerminalTabbedManager::class, this)
}
terminalTabbed.addTab(WelcomePanel())
terminalTabbed.addTab(welcomePanel)
// macOS 要避开左边的控制栏
if (SystemInfo.isMacOS) {
@@ -282,89 +113,13 @@ class TermoraFrame : JFrame() {
}
}
Disposer.register(disposable, terminalTabbed)
Disposer.register(windowScope, 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() {
val mouseAdapter = object : MouseAdapter() {
@@ -423,11 +178,25 @@ class TermoraFrame : JFrame() {
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
}
private fun initDesktopHandler() {
if (SystemInfo.isMacOS) {
FlatDesktop.setPreferencesHandler {
preferencesHandler.run()
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return dataProviderSupport.getData(dataKey)
?: terminalTabbed.getData(dataKey)
?: welcomePanel.getData(dataKey)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TermoraFrame
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}

View File

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

View File

@@ -1,16 +1,18 @@
package app.termora
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.util.SystemInfo
import com.jetbrains.WindowDecorations
import kotlinx.serialization.Serializable
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.action.ActionContainerFactory
import org.jdesktop.swingx.action.ActionManager
import java.awt.Insets
import java.awt.event.ActionEvent
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.Box
@@ -27,7 +29,7 @@ class TermoraToolBar(
private val titleBar: WindowDecorations.CustomTitleBar,
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) } }
@@ -46,8 +48,8 @@ class TermoraToolBar(
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
ToolBarAction(Actions.KEY_MANAGER, true),
ToolBarAction(Actions.MULTIPLE, true),
ToolBarAction(Actions.FIND_EVERYWHERE, true),
ToolBarAction(Actions.SETTING, true),
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
ToolBarAction(SettingsAction.SETTING, true),
)
}
@@ -96,12 +98,12 @@ class TermoraToolBar(
toolbar.removeAll()
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
override fun actionPerformed(e: ActionEvent?) {
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e)
override fun actionPerformed(evt: AnActionEvent) {
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
}
override fun isEnabled(): Boolean {
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
}
}))

View File

@@ -1,6 +1,5 @@
package app.termora
import app.termora.db.Database
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatAnimatedLafChange
import com.jthemedetecor.OsThemeDetector
@@ -24,7 +23,9 @@ class ThemeManager private constructor() {
companion object {
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(
@@ -78,7 +79,7 @@ class ThemeManager private constructor() {
GlobalScope.launch(Dispatchers.IO) {
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
override fun accept(isDark: Boolean) {
if (!Database.instance.appearance.followSystem) {
if (!Database.getDatabase().appearance.followSystem) {
return
}

View File

@@ -1,7 +1,6 @@
package app.termora
import app.termora.Application.ohMyJson
import app.termora.db.Database
import kotlinx.serialization.json.*
import okhttp3.Request
import org.apache.commons.lang3.StringUtils
@@ -19,7 +18,9 @@ import java.util.*
class UpdaterManager private constructor() {
companion object {
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(
@@ -58,7 +59,7 @@ class UpdaterManager private constructor() {
val isSelf get() = this == self
}
private val properties get() = Database.instance.properties
private val properties get() = Database.getDatabase().properties
var lastVersion = LatestVersion.self
fun fetchLatestVersion(): LatestVersion {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,29 @@
package app.termora.terminal.panel
package app.termora.actions
import app.termora.I18n
import com.formdev.flatlaf.util.SystemInfo
import org.slf4j.LoggerFactory
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.Transferable
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 {
const val COPY = "TerminalCopy"
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 systemClipboard = terminalPanel.toolkit.systemClipboard
evt.consume()
// 如果文本为空,那么清空剪切板
if (text.isEmpty()) {
@@ -30,22 +34,10 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalPre
systemClipboard.setContents(StringSelection(text), null)
terminalPanel.toast(I18n.getString("termora.terminal.copied"))
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 {
override fun getTransferDataFlavors(): Array<DataFlavor> {
@@ -61,4 +53,5 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalPre
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package app.termora.highlight
import app.termora.ApplicationScope
import app.termora.DialogWrapper
import app.termora.TerminalFactory
import com.formdev.flatlaf.util.SystemInfo
@@ -30,7 +31,8 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
override fun createCenterPanel(): JComponent {
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) {
val c = JPanel()
c.preferredSize = Dimension(24, 24)

View File

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

View File

@@ -19,8 +19,11 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
private val model = KeywordHighlightTableModel()
private val table = FlatTable()
private val keywordHighlightManager by lazy { KeywordHighlightManager.instance }
private val colorPalette by lazy { TerminalFactory.instance.createTerminal().getTerminalModel().getColorPalette() }
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
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 editBtn = JButton(I18n.getString("termora.keymgr.edit"))

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ package app.termora.highlight
import javax.swing.table.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 {
return false

View File

@@ -4,7 +4,7 @@ import app.termora.DialogWrapper
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.Icons
import app.termora.db.Database
import app.termora.Database
import app.termora.terminal.ColorPalette
import app.termora.terminal.TerminalColor
import com.formdev.flatlaf.FlatClientProperties
@@ -29,7 +29,7 @@ class NewKeywordHighlightDialog(
val colorPalette: ColorPalette
) : DialogWrapper(owner) {
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 descriptionTextField = FlatTextField()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
package app.termora.macro
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.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
@@ -18,7 +22,7 @@ class MacroDialog(owner: Window) : DialogWrapper(owner) {
private val model = DefaultListModel<Macro>()
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 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
if (index >= 0) {
val macroAction = ActionManager.getInstance().getAction(Actions.MACRO)
if (macroAction is MacroAction) {
macroAction.runMacro(model.getElementAt(index))
macroAction.runMacro(windowScope, model.getElementAt(index))
}
}
}
})
copyBtn.addActionListener {
if (list.selectionModel.selectedItemsCount > 0) {

View File

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

View File

@@ -1,6 +1,7 @@
package app.termora.macro
import app.termora.db.Database
import app.termora.ApplicationScope
import app.termora.Database
import org.slf4j.LoggerFactory
/**
@@ -8,13 +9,15 @@ import org.slf4j.LoggerFactory
*/
class MacroManager private constructor() {
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 macros = mutableMapOf<String, Macro>()
private val database get() = Database.instance
private val database get() = Database.getDatabase()
init {
macros.putAll(database.getMacros().associateBy { it.id })

View File

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

View File

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

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