diff --git a/build.gradle.kts b/build.gradle.kts index 8e2175a..ec3b4f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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("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", diff --git a/gradle.properties b/gradle.properties index 25b09e3..d6d92c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ org.gradle.caching=true org.gradle.parallel=true -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +kotlin.daemon.jvmargs=-Xmx4g \ No newline at end of file diff --git a/src/main/kotlin/app/termora/Actions.kt b/src/main/kotlin/app/termora/Actions.kt index a9977ca..f8cb7d1 100644 --- a/src/main/kotlin/app/termora/Actions.kt +++ b/src/main/kotlin/app/termora/Actions.kt @@ -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" + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/AnAction.kt b/src/main/kotlin/app/termora/AnAction.kt deleted file mode 100644 index 6f05563..0000000 --- a/src/main/kotlin/app/termora/AnAction.kt +++ /dev/null @@ -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) - -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/Application.kt b/src/main/kotlin/app/termora/Application.kt index 08a68ee..b7318ac 100644 --- a/src/main/kotlin/app/termora/Application.kt +++ b/src/main/kotlin/app/termora/Application.kt @@ -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, Any>()) private lateinit var baseDataDir: File @@ -125,22 +122,6 @@ object Application { } } - @Suppress("UNCHECKED_CAST") - fun getService(clazz: KClass): 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() diff --git a/src/main/kotlin/app/termora/ApplicationDisposable.kt b/src/main/kotlin/app/termora/ApplicationDisposable.kt deleted file mode 100644 index 7de6de4..0000000 --- a/src/main/kotlin/app/termora/ApplicationDisposable.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.termora - -/** - * 将在 JVM 进程退出时释放 - */ -class ApplicationDisposable : Disposable { - companion object { - val instance by lazy { ApplicationDisposable() } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index 5624ff3..1e71420 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -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() { - // 覆盖 tinylog 配置 - setupTinylog() + measureTimeMillis { + // 覆盖 tinylog 配置 + 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,7 +142,8 @@ class ApplicationRunner { themeManager.change(theme, true) - FlatInspector.install("ctrl shift alt X"); + if (Application.isUnknownVersion()) + FlatInspector.install("ctrl shift alt X"); UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true) UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false) @@ -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 } diff --git a/src/main/kotlin/app/termora/CustomizeToolBarDialog.kt b/src/main/kotlin/app/termora/CustomizeToolBarDialog.kt index a2b7d35..52683e3 100644 --- a/src/main/kotlin/app/termora/CustomizeToolBarDialog.kt +++ b/src/main/kotlin/app/termora/CustomizeToolBarDialog.kt @@ -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() } diff --git a/src/main/kotlin/app/termora/db/Database.kt b/src/main/kotlin/app/termora/Database.kt similarity index 87% rename from src/main/kotlin/app/termora/db/Database.kt rename to src/main/kotlin/app/termora/Database.kt index b297b8c..5b588b0 100644 --- a/src/main/kotlin/app/termora/db/Database.kt +++ b/src/main/kotlin/app/termora/Database.kt @@ -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 { + val array = env.computeInTransaction { tx -> + openCursor(tx, KEYMAP_STORE) { _, value -> + ohMyJson.decodeFromString(value) + }.values + } + + val shortcuts = mutableListOf() + 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>( + 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 { @@ -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) diff --git a/src/main/kotlin/app/termora/DialogWrapper.kt b/src/main/kotlin/app/termora/DialogWrapper.kt index d533771..73f1b47 100644 --- a/src/main/kotlin/app/termora/DialogWrapper.kt +++ b/src/main/kotlin/app/termora/DialogWrapper.kt @@ -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() } diff --git a/src/main/kotlin/app/termora/Doorman.kt b/src/main/kotlin/app/termora/Doorman.kt index 89314da..7537e4e 100644 --- a/src/main/kotlin/app/termora/Doorman.kt +++ b/src/main/kotlin/app/termora/Doorman.kt @@ -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() + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/DoormanDialog.kt b/src/main/kotlin/app/termora/DoormanDialog.kt index 7795dd7..0b5c6f0 100644 --- a/src/main/kotlin/app/termora/DoormanDialog.kt +++ b/src/main/kotlin/app/termora/DoormanDialog.kt @@ -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( diff --git a/src/main/kotlin/app/termora/EditHostOptionsPane.kt b/src/main/kotlin/app/termora/EditHostOptionsPane.kt index 07c3087..19dc6d7 100644 --- a/src/main/kotlin/app/termora/EditHostOptionsPane.kt +++ b/src/main/kotlin/app/termora/EditHostOptionsPane.kt @@ -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) diff --git a/src/main/kotlin/app/termora/HostDialog.kt b/src/main/kotlin/app/termora/HostDialog.kt index 4c7356e..abb5eb6 100644 --- a/src/main/kotlin/app/termora/HostDialog.kt +++ b/src/main/kotlin/app/termora/HostDialog.kt @@ -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 } diff --git a/src/main/kotlin/app/termora/HostManager.kt b/src/main/kotlin/app/termora/HostManager.kt index 4abd1d6..0863abc 100644 --- a/src/main/kotlin/app/termora/HostManager.kt +++ b/src/main/kotlin/app/termora/HostManager.kt @@ -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() fun addHost(host: Host, notify: Boolean = true) { diff --git a/src/main/kotlin/app/termora/HostTerminalTab.kt b/src/main/kotlin/app/termora/HostTerminalTab.kt index 6adfeae..a3573f5 100644 --- a/src/main/kotlin/app/termora/HostTerminalTab.kt +++ b/src/main/kotlin/app/termora/HostTerminalTab.kt @@ -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) diff --git a/src/main/kotlin/app/termora/HostTree.kt b/src/main/kotlin/app/termora/HostTree.kt index 6b26e48..af8db55 100644 --- a/src/main/kotlin/app/termora/HostTree.kt +++ b/src/main/kotlin/app/termora/HostTree.kt @@ -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) ) diff --git a/src/main/kotlin/app/termora/HostTreeDialog.kt b/src/main/kotlin/app/termora/HostTreeDialog.kt index 8dabdfe..fd4d2c0 100644 --- a/src/main/kotlin/app/termora/HostTreeDialog.kt +++ b/src/main/kotlin/app/termora/HostTreeDialog.kt @@ -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) ) diff --git a/src/main/kotlin/app/termora/HostTreeModel.kt b/src/main/kotlin/app/termora/HostTreeModel.kt index 163e221..d323cea 100644 --- a/src/main/kotlin/app/termora/HostTreeModel.kt +++ b/src/main/kotlin/app/termora/HostTreeModel.kt @@ -10,7 +10,7 @@ class HostTreeModel : TreeModel { val listeners = mutableListOf() - private val hostManager get() = HostManager.instance + private val hostManager get() = HostManager.getInstance() private val hosts = mutableMapOf() private val myRoot by lazy { Host( diff --git a/src/main/kotlin/app/termora/Hyperlink.kt b/src/main/kotlin/app/termora/Hyperlink.kt index 0146711..08cd51b 100644 --- a/src/main/kotlin/app/termora/Hyperlink.kt +++ b/src/main/kotlin/app/termora/Hyperlink.kt @@ -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 diff --git a/src/main/kotlin/app/termora/I18n.kt b/src/main/kotlin/app/termora/I18n.kt index 0720912..930e5c4 100644 --- a/src/main/kotlin/app/termora/I18n.kt +++ b/src/main/kotlin/app/termora/I18n.kt @@ -40,12 +40,17 @@ object I18n { } fun getString(key: String, vararg args: Any): String { + val text = getString(key) + if (args.isNotEmpty()) { + return MessageFormat.format(text, *args) + } + return text + } + + + fun getString(key: String): String { try { - val text = substitutor.replace(bundle.getString(key)) - if (args.isNotEmpty()) { - return MessageFormat.format(text, *args) - } - return text + return substitutor.replace(bundle.getString(key)) } catch (e: MissingResourceException) { if (log.isWarnEnabled) { log.warn(e.message, e) @@ -54,4 +59,5 @@ object I18n { } } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 599a1bb..7d4a032 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -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") } diff --git a/src/main/kotlin/app/termora/LocalTerminalTab.kt b/src/main/kotlin/app/termora/LocalTerminalTab.kt index a1b92c9..2912b84 100644 --- a/src/main/kotlin/app/termora/LocalTerminalTab.kt +++ b/src/main/kotlin/app/termora/LocalTerminalTab.kt @@ -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), diff --git a/src/main/kotlin/app/termora/MultiplePtyConnector.kt b/src/main/kotlin/app/termora/MultiplePtyConnector.kt index cddec2c..dcb8928 100644 --- a/src/main/kotlin/app/termora/MultiplePtyConnector.kt +++ b/src/main/kotlin/app/termora/MultiplePtyConnector.kt @@ -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) { diff --git a/src/main/kotlin/app/termora/MultipleTerminalListener.kt b/src/main/kotlin/app/termora/MultipleTerminalListener.kt index 8ffa3bc..5f36892 100644 --- a/src/main/kotlin/app/termora/MultipleTerminalListener.kt +++ b/src/main/kotlin/app/termora/MultipleTerminalListener.kt @@ -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 diff --git a/src/main/kotlin/app/termora/MyTabbedPane.kt b/src/main/kotlin/app/termora/MyTabbedPane.kt index d88c83b..c8e6eb1 100644 --- a/src/main/kotlin/app/termora/MyTabbedPane.kt +++ b/src/main/kotlin/app/termora/MyTabbedPane.kt @@ -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) } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/OpenHostActionEvent.kt b/src/main/kotlin/app/termora/OpenHostActionEvent.kt index e78e54d..0d6df45 100644 --- a/src/main/kotlin/app/termora/OpenHostActionEvent.kt +++ b/src/main/kotlin/app/termora/OpenHostActionEvent.kt @@ -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()) \ No newline at end of file +class OpenHostActionEvent(source: Any, val host: Host, event: EventObject) : + AnActionEvent(source, String(), event) \ No newline at end of file diff --git a/src/main/kotlin/app/termora/PtyConnectorFactory.kt b/src/main/kotlin/app/termora/PtyConnectorFactory.kt index a122e1a..094ff91 100644 --- a/src/main/kotlin/app/termora/PtyConnectorFactory.kt +++ b/src/main/kotlin/app/termora/PtyConnectorFactory.kt @@ -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()) - 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) diff --git a/src/main/kotlin/app/termora/PtyHostTerminalTab.kt b/src/main/kotlin/app/termora/PtyHostTerminalTab.kt index e46a12a..19c7e14 100644 --- a/src/main/kotlin/app/termora/PtyHostTerminalTab.kt +++ b/src/main/kotlin/app/termora/PtyHostTerminalTab.kt @@ -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) { diff --git a/src/main/kotlin/app/termora/SSHTerminalTab.kt b/src/main/kotlin/app/termora/SSHTerminalTab.kt index 8ec79a9..68816eb 100644 --- a/src/main/kotlin/app/termora/SSHTerminalTab.kt +++ b/src/main/kotlin/app/termora/SSHTerminalTab.kt @@ -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) } diff --git a/src/main/kotlin/app/termora/Scope.kt b/src/main/kotlin/app/termora/Scope.kt new file mode 100644 index 0000000..c871165 --- /dev/null +++ b/src/main/kotlin/app/termora/Scope.kt @@ -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, Any> = ConcurrentHashMap(), + private val properties: MutableMap = ConcurrentHashMap() +) : Disposable { + + + fun get(clazz: KClass): T { + return beans[clazz] as T + } + + + fun getOrCreate(clazz: KClass, 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() + + 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 { + 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 { + 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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/SearchableHostTreeModel.kt b/src/main/kotlin/app/termora/SearchableHostTreeModel.kt index 08bee70..7f8b5ed 100644 --- a/src/main/kotlin/app/termora/SearchableHostTreeModel.kt +++ b/src/main/kotlin/app/termora/SearchableHostTreeModel.kt @@ -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().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().any { it.name.contains(text, true) || it.host.contains(text, true) } } } diff --git a/src/main/kotlin/app/termora/SettingsDialog.kt b/src/main/kotlin/app/termora/SettingsDialog.kt index 9f0e9f7..8327a7e 100644 --- a/src/main/kotlin/app/termora/SettingsDialog.kt +++ b/src/main/kotlin/app/termora/SettingsDialog.kt @@ -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")) diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index ed204dc..111031d 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -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() val languageComboBox = FlatComboBox() 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() 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 + } + + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalFactory.kt b/src/main/kotlin/app/termora/TerminalFactory.kt index 214201c..888cc39 100644 --- a/src/main/kotlin/app/termora/TerminalFactory.kt +++ b/src/main/kotlin/app/termora/TerminalFactory.kt @@ -1,19 +1,21 @@ 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() 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 } } + + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalPanelFactory.kt b/src/main/kotlin/app/termora/TerminalPanelFactory.kt index d63a09f..025f83c 100644 --- a/src/main/kotlin/app/termora/TerminalPanelFactory.kt +++ b/src/main/kotlin/app/termora/TerminalPanelFactory.kt @@ -13,14 +13,16 @@ class TerminalPanelFactory { private val terminalPanels = mutableListOf() 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 } diff --git a/src/main/kotlin/app/termora/TerminalTabDialog.kt b/src/main/kotlin/app/termora/TerminalTabDialog.kt index a82077c..31ddd3f 100644 --- a/src/main/kotlin/app/termora/TerminalTabDialog.kt +++ b/src/main/kotlin/app/termora/TerminalTabDialog.kt @@ -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.dispose() } + override fun getData(dataKey: DataKey): T? { + return dataProviderSupport.getData(dataKey) + } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalTabbed.kt b/src/main/kotlin/app/termora/TerminalTabbed.kt index e722755..c4c9dfa 100644 --- a/src/main/kotlin/app/termora/TerminalTabbed.kt +++ b/src/main/kotlin/app/termora/TerminalTabbed.kt @@ -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() 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,44 +118,35 @@ class TerminalTabbed( }) // 注册全局搜索 - FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider { - override fun find(pattern: String): List { - val results = mutableListOf() - for (i in 0 until tabbedPane.tabCount) { - val c = tabbedPane.getComponentAt(i) - if (c is WelcomePanel || c is TransportPanel) { - continue - } - results.add( - SwitchFindEverywhereResult( - tabbedPane.getTitleAt(i), - tabbedPane.getIconAt(i), - tabbedPane.getComponentAt(i) + FindEverywhereProvider.getFindEverywhereProviders(windowScope) + .add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider { + override fun find(pattern: String): List { + val results = mutableListOf() + for (i in 0 until tabbedPane.tabCount) { + val c = tabbedPane.getComponentAt(i) + if (c is WelcomePanel || c is TransportPanel) { + continue + } + results.add( + SwitchFindEverywhereResult( + tabbedPane.getTitleAt(i), + tabbedPane.getIconAt(i), + tabbedPane.getComponentAt(i) + ) ) - ) + } + return results } - return results - } - override fun group(): String { - return I18n.getString("termora.find-everywhere.groups.opened-hosts") - } - - override fun order(): Int { - return Integer.MIN_VALUE + 1 - } - })) - - - // 打开 Host - ActionManager.getInstance().addAction(Actions.OPEN_HOST, object : AbstractAction() { - override fun actionPerformed(e: ActionEvent) { - if (e !is OpenHostActionEvent) { - return + override fun group(): String { + return I18n.getString("termora.find-everywhere.groups.opened-hosts") } - openHost(e.host) - } - }) + + override fun order(): Int { + return Integer.MIN_VALUE + 1 + } + })) + // 监听全局事件 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) - val dialog = TerminalTabDialog( - owner = SwingUtilities.getWindowAncestor(this), - terminalTab = tab, - size = Dimension(min(size.width, 1280), min(size.height, 800)) - ) - dialog.title = title - Disposer.register(dialog, tab) - Disposer.register(this, dialog) - dialog.isVisible = true + 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 = owner, + terminalTab = tab, + size = Dimension(min(size.width, 1280), min(size.height, 800)) + ) + dialog.title = title + Disposer.register(dialog, tab) + 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 getData(dataKey: DataKey): T? { + return dataProviderSupport.getData(dataKey) + } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TerminalTabbedManager.kt b/src/main/kotlin/app/termora/TerminalTabbedManager.kt index 9ba4c2d..0ccbda5 100644 --- a/src/main/kotlin/app/termora/TerminalTabbedManager.kt +++ b/src/main/kotlin/app/termora/TerminalTabbedManager.kt @@ -5,4 +5,5 @@ interface TerminalTabbedManager { fun getSelectedTerminalTab(): TerminalTab? fun getTerminalTabs(): List fun setSelectedTerminalTab(tab: TerminalTab) + fun closeTerminalTab(tab: TerminalTab) } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraFrame.kt b/src/main/kotlin/app/termora/TermoraFrame.kt index cfea4dc..ce5e5be 100644 --- a/src/main/kotlin/app/termora/TermoraFrame.kt +++ b/src/main/kotlin/app/termora/TermoraFrame.kt @@ -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 getData(dataKey: DataKey): 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() + } + + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraFrameManager.kt b/src/main/kotlin/app/termora/TermoraFrameManager.kt new file mode 100644 index 0000000..d6b7d0f --- /dev/null +++ b/src/main/kotlin/app/termora/TermoraFrameManager.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/TermoraToolBar.kt b/src/main/kotlin/app/termora/TermoraToolBar.kt index 4df526f..d5646ea 100644 --- a/src/main/kotlin/app/termora/TermoraToolBar.kt +++ b/src/main/kotlin/app/termora/TermoraToolBar.kt @@ -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 } })) diff --git a/src/main/kotlin/app/termora/ThemeManager.kt b/src/main/kotlin/app/termora/ThemeManager.kt index 28cf372..8de2f5c 100644 --- a/src/main/kotlin/app/termora/ThemeManager.kt +++ b/src/main/kotlin/app/termora/ThemeManager.kt @@ -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 { override fun accept(isDark: Boolean) { - if (!Database.instance.appearance.followSystem) { + if (!Database.getDatabase().appearance.followSystem) { return } diff --git a/src/main/kotlin/app/termora/UpdaterManager.kt b/src/main/kotlin/app/termora/UpdaterManager.kt index 4bcadf5..d730542 100644 --- a/src/main/kotlin/app/termora/UpdaterManager.kt +++ b/src/main/kotlin/app/termora/UpdaterManager.kt @@ -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 { diff --git a/src/main/kotlin/app/termora/WelcomePanel.kt b/src/main/kotlin/app/termora/WelcomePanel.kt index 8dd92c8..63c7257 100644 --- a/src/main/kotlin/app/termora/WelcomePanel.kt +++ b/src/main/kotlin/app/termora/WelcomePanel.kt @@ -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,31 +163,23 @@ 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) + FindEverywhereProvider.getFindEverywhereProviders(windowScope) + .add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider { + override fun find(pattern: String): List { + return TreeUtils.children(hostTree.model, hostTree.model.root) + .filterIsInstance() + .filter { it.protocol != Protocol.Folder } + .map { HostFindEverywhereResult(it) } } - hostTree.showAddHostDialog() - } - }) - FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider { - override fun find(pattern: String): List { - return TreeUtils.children(hostTree.model, hostTree.model.root) - .filterIsInstance() - .filter { it.protocol != Protocol.Folder } - .map { HostFindEverywhereResult(it) } - } + override fun group(): String { + return I18n.getString("termora.find-everywhere.groups.open-new-hosts") + } - override fun group(): String { - return I18n.getString("termora.find-everywhere.groups.open-new-hosts") - } - - override fun order(): Int { - return Integer.MIN_VALUE + 2 - } - })) + override fun order(): Int { + return Integer.MIN_VALUE + 2 + } + })) searchTextField.document.addDocumentListener(object : DocumentAdaptor() { private var state = StringUtils.EMPTY @@ -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 getData(dataKey: DataKey): T? { + return dataProviderSupport.getData(dataKey) + } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/ActionManager.kt b/src/main/kotlin/app/termora/actions/ActionManager.kt new file mode 100644 index 0000000..fdc73f0 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/ActionManager.kt @@ -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) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/AnAction.kt b/src/main/kotlin/app/termora/actions/AnAction.kt new file mode 100644 index 0000000..7c39d5a --- /dev/null +++ b/src/main/kotlin/app/termora/actions/AnAction.kt @@ -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) + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/AnActionEvent.kt b/src/main/kotlin/app/termora/actions/AnActionEvent.kt new file mode 100644 index 0000000..48cc151 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/AnActionEvent.kt @@ -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 getData(dataKey: DataKey): 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 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/AppUpdateAction.kt b/src/main/kotlin/app/termora/actions/AppUpdateAction.kt new file mode 100644 index 0000000..51dc3e2 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/AppUpdateAction.kt @@ -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}")) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/DataProvider.kt b/src/main/kotlin/app/termora/actions/DataProvider.kt new file mode 100644 index 0000000..beb105c --- /dev/null +++ b/src/main/kotlin/app/termora/actions/DataProvider.kt @@ -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 getData(dataKey: DataKey): T? { + return null + } + } + } + + /** + * 数据提供 + */ + fun getData(dataKey: DataKey): T? +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/DataProviderSupport.kt b/src/main/kotlin/app/termora/actions/DataProviderSupport.kt new file mode 100644 index 0000000..2db38f9 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/DataProviderSupport.kt @@ -0,0 +1,24 @@ +package app.termora.actions + +import app.termora.terminal.DataKey + +class DataProviderSupport : DataProvider { + private val map = mutableMapOf, Any>() + + override fun getData(dataKey: DataKey): T? { + if (map.containsKey(dataKey)) { + @Suppress("UNCHECKED_CAST") + return map[dataKey] as T + } + + return null + } + + fun addData(key: DataKey, data: T) { + map[key] = data + } + + fun removeData(key: DataKey<*>) { + map.remove(key) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/DataProviders.kt b/src/main/kotlin/app/termora/actions/DataProviders.kt new file mode 100644 index 0000000..311dd2d --- /dev/null +++ b/src/main/kotlin/app/termora/actions/DataProviders.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/MultipleAction.kt b/src/main/kotlin/app/termora/actions/MultipleAction.kt new file mode 100644 index 0000000..9a28835 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/MultipleAction.kt @@ -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() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/NewHostAction.kt b/src/main/kotlin/app/termora/actions/NewHostAction.kt new file mode 100644 index 0000000..ce1f5da --- /dev/null +++ b/src/main/kotlin/app/termora/actions/NewHostAction.kt @@ -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)) + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/NewWindowAction.kt b/src/main/kotlin/app/termora/actions/NewWindowAction.kt new file mode 100644 index 0000000..697f534 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/NewWindowAction.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/OpenHostAction.kt b/src/main/kotlin/app/termora/actions/OpenHostAction.kt new file mode 100644 index 0000000..fb0986a --- /dev/null +++ b/src/main/kotlin/app/termora/actions/OpenHostAction.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/OpenLocalTerminalAction.kt b/src/main/kotlin/app/termora/actions/OpenLocalTerminalAction.kt new file mode 100644 index 0000000..44e4ac9 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/OpenLocalTerminalAction.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/SettingsAction.kt b/src/main/kotlin/app/termora/actions/SettingsAction.kt new file mode 100644 index 0000000..d4b6b96 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/SettingsAction.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/SwitchTabAction.kt b/src/main/kotlin/app/termora/actions/SwitchTabAction.kt new file mode 100644 index 0000000..a4deac9 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/SwitchTabAction.kt @@ -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() + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalClearScreenAction.kt b/src/main/kotlin/app/termora/actions/TerminalClearScreenAction.kt new file mode 100644 index 0000000..bdb2202 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalClearScreenAction.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalCloseAction.kt b/src/main/kotlin/app/termora/actions/TerminalCloseAction.kt new file mode 100644 index 0000000..1c1987c --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalCloseAction.kt @@ -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() + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalCopyAction.kt b/src/main/kotlin/app/termora/actions/TerminalCopyAction.kt similarity index 54% rename from src/main/kotlin/app/termora/terminal/panel/TerminalCopyAction.kt rename to src/main/kotlin/app/termora/actions/TerminalCopyAction.kt index b588def..5b3c63d 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalCopyAction.kt +++ b/src/main/kotlin/app/termora/actions/TerminalCopyAction.kt @@ -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 { @@ -61,4 +53,5 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalPre } } + } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalFindAction.kt b/src/main/kotlin/app/termora/actions/TerminalFindAction.kt new file mode 100644 index 0000000..ad6b45a --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalFindAction.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalPasteAction.kt b/src/main/kotlin/app/termora/actions/TerminalPasteAction.kt new file mode 100644 index 0000000..3d006f7 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalPasteAction.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalSelectAllAction.kt b/src/main/kotlin/app/termora/actions/TerminalSelectAllAction.kt new file mode 100644 index 0000000..5b2147c --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalSelectAllAction.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalZoomAction.kt b/src/main/kotlin/app/termora/actions/TerminalZoomAction.kt new file mode 100644 index 0000000..6764bc1 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalZoomAction.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalZoomInAction.kt b/src/main/kotlin/app/termora/actions/TerminalZoomInAction.kt new file mode 100644 index 0000000..ea3b3b7 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalZoomInAction.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalZoomOutAction.kt b/src/main/kotlin/app/termora/actions/TerminalZoomOutAction.kt new file mode 100644 index 0000000..513d665 --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalZoomOutAction.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/TerminalZoomResetAction.kt b/src/main/kotlin/app/termora/actions/TerminalZoomResetAction.kt new file mode 100644 index 0000000..941f8ca --- /dev/null +++ b/src/main/kotlin/app/termora/actions/TerminalZoomResetAction.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt b/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt index f9ddd02..1d0a708 100644 --- a/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt +++ b/src/main/kotlin/app/termora/findeverywhere/FindEverywhere.kt @@ -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,24 +23,13 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) { private val model = DefaultListModel() private val resultList = FindEverywhereXList(model) private val centerPanel = JPanel(BorderLayout()) + private val providers = mutableListOf( + BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()), + BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()), + BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider()), + BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()), + ) - companion object { - private val providers = mutableListOf( - BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()), - BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()), - BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider()), - 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) { } }) + } + fun registerProvider(provider: FindEverywhereProvider) { + providers.add(provider) + providers.sortBy { it.order() } + } - 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 unregisterProvider(provider: FindEverywhereProvider) { + providers.remove(provider) } override fun createCenterPanel(): JComponent { diff --git a/src/main/kotlin/app/termora/findeverywhere/FindEverywhereAction.kt b/src/main/kotlin/app/termora/findeverywhere/FindEverywhereAction.kt new file mode 100644 index 0000000..5531e85 --- /dev/null +++ b/src/main/kotlin/app/termora/findeverywhere/FindEverywhereAction.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/findeverywhere/FindEverywhereProvider.kt b/src/main/kotlin/app/termora/findeverywhere/FindEverywhereProvider.kt index efaabc1..2105484 100644 --- a/src/main/kotlin/app/termora/findeverywhere/FindEverywhereProvider.kt +++ b/src/main/kotlin/app/termora/findeverywhere/FindEverywhereProvider.kt @@ -1,7 +1,21 @@ package app.termora.findeverywhere +import app.termora.Scope + interface FindEverywhereProvider { + companion object { + @Suppress("UNCHECKED_CAST") + fun getFindEverywhereProviders(scope: Scope): MutableList { + var list = scope.getAnyOrNull("FindEverywhereProviders") + if (list == null) { + list = mutableListOf() + scope.putAny("FindEverywhereProviders", list) + } + return list as MutableList + } + } + /** * 搜索 */ diff --git a/src/main/kotlin/app/termora/findeverywhere/QuickActionsFindEverywhereProvider.kt b/src/main/kotlin/app/termora/findeverywhere/QuickActionsFindEverywhereProvider.kt index ad98daf..4a01969 100644 --- a/src/main/kotlin/app/termora/findeverywhere/QuickActionsFindEverywhereProvider.kt +++ b/src/main/kotlin/app/termora/findeverywhere/QuickActionsFindEverywhereProvider.kt @@ -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 { diff --git a/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt b/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt index 6cb9eae..c08dce3 100644 --- a/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt +++ b/src/main/kotlin/app/termora/findeverywhere/QuickCommandFindEverywhereProvider.kt @@ -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 { val list = mutableListOf() - 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) { diff --git a/src/main/kotlin/app/termora/highlight/ChooseColorTemplateDialog.kt b/src/main/kotlin/app/termora/highlight/ChooseColorTemplateDialog.kt index f38d621..7890abf 100644 --- a/src/main/kotlin/app/termora/highlight/ChooseColorTemplateDialog.kt +++ b/src/main/kotlin/app/termora/highlight/ChooseColorTemplateDialog.kt @@ -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) diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlightAction.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlightAction.kt new file mode 100644 index 0000000..9225e52 --- /dev/null +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlightAction.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlightDialog.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlightDialog.kt index 9b74134..6d3994b 100644 --- a/src/main/kotlin/app/termora/highlight/KeywordHighlightDialog.kt +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlightDialog.kt @@ -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")) diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt index 9e78e52..60f9ca1 100644 --- a/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt @@ -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() 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) diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlightPaintListener.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlightPaintListener.kt index 39bb8e9..fae45a0 100644 --- a/src/main/kotlin/app/termora/highlight/KeywordHighlightPaintListener.kt +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlightPaintListener.kt @@ -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, diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlightTableModel.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlightTableModel.kt index 651830b..91e0863 100644 --- a/src/main/kotlin/app/termora/highlight/KeywordHighlightTableModel.kt +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlightTableModel.kt @@ -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 diff --git a/src/main/kotlin/app/termora/highlight/NewKeywordHighlightDialog.kt b/src/main/kotlin/app/termora/highlight/NewKeywordHighlightDialog.kt index b88b172..232d932 100644 --- a/src/main/kotlin/app/termora/highlight/NewKeywordHighlightDialog.kt +++ b/src/main/kotlin/app/termora/highlight/NewKeywordHighlightDialog.kt @@ -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() diff --git a/src/main/kotlin/app/termora/keymap/KeyShortcut.kt b/src/main/kotlin/app/termora/keymap/KeyShortcut.kt new file mode 100644 index 0000000..3da8fb7 --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/KeyShortcut.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymap/Keymap.kt b/src/main/kotlin/app/termora/keymap/Keymap.kt new file mode 100644 index 0000000..b680935 --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/Keymap.kt @@ -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>() + + 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 { + val shortcuts = mutableListOf() + 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> { + val shortcuts = mutableMapOf>() + shortcuts.putAll(this.shortcuts) + parent?.let { shortcuts.putAll(it.getShortcuts()) } + return shortcuts + } + + open fun getActionIds(shortcut: Shortcut): List { + val actionIds = mutableListOf() + 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)) + }) + } + }) + }) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymap/KeymapImpl.kt b/src/main/kotlin/app/termora/keymap/KeymapImpl.kt new file mode 100644 index 0000000..09d6e45 --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/KeymapImpl.kt @@ -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)) + ) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymap/KeymapManager.kt b/src/main/kotlin/app/termora/keymap/KeymapManager.kt new file mode 100644 index 0000000..8cab245 --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/KeymapManager.kt @@ -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() + 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 { + 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymap/KeymapPanel.kt b/src/main/kotlin/app/termora/keymap/KeymapPanel.kt new file mode 100644 index 0000000..6c4c273 --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/KeymapPanel.kt @@ -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() + 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() + + 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 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymap/KeymapTableModel.kt b/src/main/kotlin/app/termora/keymap/KeymapTableModel.kt new file mode 100644 index 0000000..f464d0b --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/KeymapTableModel.kt @@ -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(" + ") + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymap/MacOSKeymap.kt b/src/main/kotlin/app/termora/keymap/MacOSKeymap.kt new file mode 100644 index 0000000..049c894 --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/MacOSKeymap.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymap/Shortcut.kt b/src/main/kotlin/app/termora/keymap/Shortcut.kt new file mode 100644 index 0000000..c10945c --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/Shortcut.kt @@ -0,0 +1,6 @@ +package app.termora.keymap + +abstract class Shortcut { + abstract fun isKeyboard(): Boolean +} + diff --git a/src/main/kotlin/app/termora/keymap/WindowsKeymap.kt b/src/main/kotlin/app/termora/keymap/WindowsKeymap.kt new file mode 100644 index 0000000..efe4fad --- /dev/null +++ b/src/main/kotlin/app/termora/keymap/WindowsKeymap.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymgr/KeyManager.kt b/src/main/kotlin/app/termora/keymgr/KeyManager.kt index c56798a..f10a30d 100644 --- a/src/main/kotlin/app/termora/keymgr/KeyManager.kt +++ b/src/main/kotlin/app/termora/keymgr/KeyManager.kt @@ -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() - private val database get() = Database.instance + private val database get() = Database.getDatabase() init { keyPairs.addAll(database.getKeyPairs()) diff --git a/src/main/kotlin/app/termora/keymgr/KeyManagerAction.kt b/src/main/kotlin/app/termora/keymgr/KeyManagerAction.kt new file mode 100644 index 0000000..1f6c3c0 --- /dev/null +++ b/src/main/kotlin/app/termora/keymgr/KeyManagerAction.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt index 216f7bc..56e8446 100644 --- a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt +++ b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt @@ -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) } diff --git a/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt b/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt index 47870bf..d3f2447 100644 --- a/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt +++ b/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt @@ -46,7 +46,7 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair override fun loadKeys(session: SessionContext?): Iterable { 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") diff --git a/src/main/kotlin/app/termora/macro/MacroAction.kt b/src/main/kotlin/app/termora/macro/MacroAction.kt index da1f859..710c077 100644 --- a/src/main/kotlin/app/termora/macro/MacroAction.kt +++ b/src/main/kotlin/app/termora/macro/MacroAction.kt @@ -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) { diff --git a/src/main/kotlin/app/termora/macro/MacroDialog.kt b/src/main/kotlin/app/termora/macro/MacroDialog.kt index d7da741..b039d8c 100644 --- a/src/main/kotlin/app/termora/macro/MacroDialog.kt +++ b/src/main/kotlin/app/termora/macro/MacroDialog.kt @@ -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() 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 { - val index = list.selectedIndex - if (index >= 0) { - val macroAction = ActionManager.getInstance().getAction(Actions.MACRO) - if (macroAction is MacroAction) { - macroAction.runMacro(model.getElementAt(index)) + 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(windowScope, model.getElementAt(index)) + } } } - } + }) copyBtn.addActionListener { if (list.selectionModel.selectedItemsCount > 0) { diff --git a/src/main/kotlin/app/termora/macro/MacroFindEverywhereProvider.kt b/src/main/kotlin/app/termora/macro/MacroFindEverywhereProvider.kt index 892bb9b..92b49a3 100644 --- a/src/main/kotlin/app/termora/macro/MacroFindEverywhereProvider.kt +++ b/src/main/kotlin/app/termora/macro/MacroFindEverywhereProvider.kt @@ -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 { 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 { diff --git a/src/main/kotlin/app/termora/macro/MacroManager.kt b/src/main/kotlin/app/termora/macro/MacroManager.kt index 680b06b..723405e 100644 --- a/src/main/kotlin/app/termora/macro/MacroManager.kt +++ b/src/main/kotlin/app/termora/macro/MacroManager.kt @@ -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() - private val database get() = Database.instance + private val database get() = Database.getDatabase() init { macros.putAll(database.getMacros().associateBy { it.id }) diff --git a/src/main/kotlin/app/termora/macro/MacroPlaybackAction.kt b/src/main/kotlin/app/termora/macro/MacroPlaybackAction.kt index e983cb1..f32027c 100644 --- a/src/main/kotlin/app/termora/macro/MacroPlaybackAction.kt +++ b/src/main/kotlin/app/termora/macro/MacroPlaybackAction.kt @@ -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 { diff --git a/src/main/kotlin/app/termora/macro/MacroPtyConnector.kt b/src/main/kotlin/app/termora/macro/MacroPtyConnector.kt index b4581de..59b3b21 100644 --- a/src/main/kotlin/app/termora/macro/MacroPtyConnector.kt +++ b/src/main/kotlin/app/termora/macro/MacroPtyConnector.kt @@ -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 diff --git a/src/main/kotlin/app/termora/macro/MacroStartRecordingAction.kt b/src/main/kotlin/app/termora/macro/MacroStartRecordingAction.kt index c4c96ba..a087dda 100644 --- a/src/main/kotlin/app/termora/macro/MacroStartRecordingAction.kt +++ b/src/main/kotlin/app/termora/macro/MacroStartRecordingAction.kt @@ -1,11 +1,11 @@ package app.termora.macro import app.termora.Actions -import app.termora.AnAction import app.termora.I18n import app.termora.Icons +import app.termora.actions.AnAction +import app.termora.actions.AnActionEvent import org.jdesktop.swingx.action.ActionManager -import java.awt.event.ActionEvent import javax.swing.Icon class MacroStartRecordingAction(icon: Icon = Icons.rec) : AnAction( @@ -14,7 +14,7 @@ class MacroStartRecordingAction(icon: Icon = Icons.rec) : AnAction( ) { private val macroAction get() = ActionManager.getInstance().getAction(Actions.MACRO) as MacroAction? - override fun actionPerformed(evt: ActionEvent) { + override fun actionPerformed(evt: AnActionEvent) { macroAction?.startRecording() } diff --git a/src/main/kotlin/app/termora/macro/MacroStopRecordingAction.kt b/src/main/kotlin/app/termora/macro/MacroStopRecordingAction.kt index 42a0d2c..054728b 100644 --- a/src/main/kotlin/app/termora/macro/MacroStopRecordingAction.kt +++ b/src/main/kotlin/app/termora/macro/MacroStopRecordingAction.kt @@ -1,11 +1,11 @@ package app.termora.macro import app.termora.Actions -import app.termora.AnAction import app.termora.I18n import app.termora.Icons +import app.termora.actions.AnAction +import app.termora.actions.AnActionEvent import org.jdesktop.swingx.action.ActionManager -import java.awt.event.ActionEvent import javax.swing.Icon class MacroStopRecordingAction(icon: Icon = Icons.stop) : AnAction( @@ -14,7 +14,7 @@ class MacroStopRecordingAction(icon: Icon = Icons.stop) : AnAction( ) { private val macroAction get() = ActionManager.getInstance().getAction(Actions.MACRO) as MacroAction? - override fun actionPerformed(evt: ActionEvent) { + override fun actionPerformed(evt: AnActionEvent) { macroAction?.stopRecording() } diff --git a/src/main/kotlin/app/termora/native/FileChooser.kt b/src/main/kotlin/app/termora/native/FileChooser.kt index 2a0bccc..2f97921 100644 --- a/src/main/kotlin/app/termora/native/FileChooser.kt +++ b/src/main/kotlin/app/termora/native/FileChooser.kt @@ -85,7 +85,7 @@ class FileChooser { private fun showMacOSOpenDialog(future: CompletableFuture>) { - DispatchNative.instance.dispatch_async(object : Runnable { + DispatchNative.getInstance().dispatch_async(object : Runnable { override fun run() { val pool = Foundation.NSAutoreleasePool() try { @@ -161,7 +161,7 @@ class FileChooser { } private fun showMacOSSaveDialog(filename: String, future: CompletableFuture) { - DispatchNative.instance.dispatch_async(object : Runnable { + DispatchNative.getInstance().dispatch_async(object : Runnable { override fun run() { val pool = Foundation.NSAutoreleasePool() try { diff --git a/src/main/kotlin/app/termora/native/osx/DispatchNative.kt b/src/main/kotlin/app/termora/native/osx/DispatchNative.kt index b84a553..97a7edf 100644 --- a/src/main/kotlin/app/termora/native/osx/DispatchNative.kt +++ b/src/main/kotlin/app/termora/native/osx/DispatchNative.kt @@ -1,10 +1,13 @@ package app.termora.native.osx +import app.termora.ApplicationScope import java.lang.reflect.Method class DispatchNative private constructor() { companion object { - val instance by lazy { DispatchNative() } + fun getInstance(): DispatchNative { + return ApplicationScope.forApplicationScope().getOrCreate(DispatchNative::class) { DispatchNative() } + } } val dispatch_main_queue: Long diff --git a/src/main/kotlin/app/termora/native/osx/MacOSKeyStorage.kt b/src/main/kotlin/app/termora/native/osx/MacOSKeyStorage.kt index b898d40..5f4d830 100644 --- a/src/main/kotlin/app/termora/native/osx/MacOSKeyStorage.kt +++ b/src/main/kotlin/app/termora/native/osx/MacOSKeyStorage.kt @@ -1,5 +1,6 @@ package app.termora.native.osx +import app.termora.ApplicationScope import app.termora.native.KeyStorage import com.sun.jna.Library import com.sun.jna.Memory @@ -13,7 +14,9 @@ class MacOSKeyStorage private constructor() : KeyStorage { companion object { - val instance by lazy { MacOSKeyStorage() } + fun getInstance(): MacOSKeyStorage { + return ApplicationScope.forApplicationScope().getOrCreate(MacOSKeyStorage::class) { MacOSKeyStorage() } + } private val errSecItemNotFound = -25300 private val errSecSuccess = 0 diff --git a/src/main/kotlin/app/termora/sync/GitHubSyncer.kt b/src/main/kotlin/app/termora/sync/GitHubSyncer.kt index b1464cb..9b51fce 100644 --- a/src/main/kotlin/app/termora/sync/GitHubSyncer.kt +++ b/src/main/kotlin/app/termora/sync/GitHubSyncer.kt @@ -1,6 +1,7 @@ package app.termora.sync import app.termora.Application.ohMyJson +import app.termora.ApplicationScope import app.termora.ResponseException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* @@ -11,7 +12,9 @@ import okhttp3.Response class GitHubSyncer private constructor() : GitSyncer() { companion object { - val instance by lazy { GitHubSyncer() } + fun getInstance(): GitHubSyncer { + return ApplicationScope.forApplicationScope().getOrCreate(GitHubSyncer::class) { GitHubSyncer() } + } } override fun newPullRequestBuilder(config: SyncConfig): Request.Builder { diff --git a/src/main/kotlin/app/termora/sync/GitLabSyncer.kt b/src/main/kotlin/app/termora/sync/GitLabSyncer.kt index 3d7e9d3..50f6c34 100644 --- a/src/main/kotlin/app/termora/sync/GitLabSyncer.kt +++ b/src/main/kotlin/app/termora/sync/GitLabSyncer.kt @@ -1,6 +1,7 @@ package app.termora.sync import app.termora.Application.ohMyJson +import app.termora.ApplicationScope import app.termora.ResponseException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* @@ -14,7 +15,9 @@ import java.nio.charset.StandardCharsets class GitLabSyncer private constructor() : GitSyncer() { companion object { - val instance by lazy { GitLabSyncer() } + fun getInstance(): GitLabSyncer { + return ApplicationScope.forApplicationScope().getOrCreate(GitLabSyncer::class) { GitLabSyncer() } + } } private val SyncConfig.domain get() = options.getValue("domain") diff --git a/src/main/kotlin/app/termora/sync/GitSyncer.kt b/src/main/kotlin/app/termora/sync/GitSyncer.kt index 6aea087..903f6ad 100644 --- a/src/main/kotlin/app/termora/sync/GitSyncer.kt +++ b/src/main/kotlin/app/termora/sync/GitSyncer.kt @@ -28,10 +28,10 @@ abstract class GitSyncer : Syncer { protected val description = "${Application.getName()} config" protected val httpClient get() = Application.httpClient - protected val hostManager get() = HostManager.instance - protected val keyManager get() = KeyManager.instance - protected val keywordHighlightManager get() = KeywordHighlightManager.instance - protected val macroManager get() = MacroManager.instance + protected val hostManager get() = HostManager.getInstance() + protected val keyManager get() = KeyManager.getInstance() + protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance() + protected val macroManager get() = MacroManager.getInstance() override fun pull(config: SyncConfig): GistResponse { diff --git a/src/main/kotlin/app/termora/sync/GiteeSyncer.kt b/src/main/kotlin/app/termora/sync/GiteeSyncer.kt index 4c8a933..717b7c3 100644 --- a/src/main/kotlin/app/termora/sync/GiteeSyncer.kt +++ b/src/main/kotlin/app/termora/sync/GiteeSyncer.kt @@ -1,6 +1,7 @@ package app.termora.sync import app.termora.Application.ohMyJson +import app.termora.ApplicationScope import app.termora.ResponseException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* @@ -13,7 +14,9 @@ import org.apache.commons.lang3.StringUtils class GiteeSyncer private constructor() : GitSyncer() { companion object { - val instance by lazy { GiteeSyncer() } + fun getInstance(): GiteeSyncer { + return ApplicationScope.forApplicationScope().getOrCreate(GiteeSyncer::class) { GiteeSyncer() } + } } override fun newPullRequestBuilder(config: SyncConfig): Request.Builder { diff --git a/src/main/kotlin/app/termora/sync/SyncerProvider.kt b/src/main/kotlin/app/termora/sync/SyncerProvider.kt index 55d7230..6565e2a 100644 --- a/src/main/kotlin/app/termora/sync/SyncerProvider.kt +++ b/src/main/kotlin/app/termora/sync/SyncerProvider.kt @@ -1,19 +1,20 @@ package app.termora.sync +import app.termora.ApplicationScope + class SyncerProvider private constructor() { companion object { - val instance by lazy { SyncerProvider() } + fun getInstance(): SyncerProvider { + return ApplicationScope.forApplicationScope().getOrCreate(SyncerProvider::class) { SyncerProvider() } + } } fun getSyncer(type: SyncType): Syncer { - if (type == SyncType.GitHub) { - return GitHubSyncer.instance - } else if (type == SyncType.Gitee) { - return GiteeSyncer.instance - }else if (type == SyncType.GitLab) { - return GitLabSyncer.instance + return when (type) { + SyncType.GitHub -> GitHubSyncer.getInstance() + SyncType.Gitee -> GiteeSyncer.getInstance() + SyncType.GitLab -> GitLabSyncer.getInstance() } - throw UnsupportedOperationException("Type $type is not supported.") } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/ColorPaletteImpl.kt b/src/main/kotlin/app/termora/terminal/ColorPaletteImpl.kt index 34f3002..e023dd2 100644 --- a/src/main/kotlin/app/termora/terminal/ColorPaletteImpl.kt +++ b/src/main/kotlin/app/termora/terminal/ColorPaletteImpl.kt @@ -1,5 +1,6 @@ package app.termora.terminal +import app.termora.ApplicationScope import java.awt.Color interface ColorTheme { @@ -13,7 +14,9 @@ interface ColorTheme { open class DefaultColorTheme : ColorTheme { companion object { - val instance by lazy { DefaultColorTheme() } + fun getInstance(): DefaultColorTheme { + return ApplicationScope.forApplicationScope().getOrCreate(DefaultColorTheme::class) { DefaultColorTheme() } + } } override fun getColor(color: TerminalColor): Int { diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt index 0f6e3d2..9fc6302 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalDisplay.kt @@ -2,7 +2,7 @@ package app.termora.terminal.panel import app.termora.DynamicColor import app.termora.assertEventDispatchThread -import app.termora.db.Database +import app.termora.Database import app.termora.terminal.* import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing @@ -175,8 +175,8 @@ class TerminalDisplay( private fun checkFont() { // 如果字体已经改变,那么这里刷新字体 - if (font.family != Database.instance.terminal.font - || font.size != Database.instance.terminal.fontSize + if (font.family != Database.getDatabase().terminal.font + || font.size != Database.getDatabase().terminal.fontSize ) { font = getTerminalFont() monospacedFont = Font(Font.MONOSPACED, font.style, font.size) @@ -419,7 +419,11 @@ class TerminalDisplay( private fun getTerminalFont(): Font { - return Font(Database.instance.terminal.font, Font.PLAIN, Database.instance.terminal.fontSize) + return Font( + Database.getDatabase().terminal.font, + Font.PLAIN, + Database.getDatabase().terminal.fontSize + ) } fun toast(text: String, duration: Duration) { diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalFindAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalFindAction.kt deleted file mode 100644 index d7bc965..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalFindAction.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.termora.terminal.panel - -import java.awt.Toolkit -import java.awt.event.KeyEvent -import javax.swing.KeyStroke - -class TerminalFindAction(private val terminalPanel: TerminalPanel) : TerminalAction( - KeyStroke.getKeyStroke( - KeyEvent.VK_F, -Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx ) -) { - override fun actionPerformed(e: KeyEvent) { - terminalPanel.showFind() - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalHyperlinkPaintListener.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalHyperlinkPaintListener.kt index 2725468..22f76ca 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalHyperlinkPaintListener.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalHyperlinkPaintListener.kt @@ -1,6 +1,7 @@ package app.termora.terminal.panel import app.termora.Application +import app.termora.ApplicationScope import app.termora.terminal.* import java.awt.Graphics import java.net.URI @@ -8,7 +9,10 @@ import kotlin.math.min class TerminalHyperlinkPaintListener private constructor() : TerminalPaintListener { companion object { - val instance by lazy { TerminalHyperlinkPaintListener() } + fun getInstance(): TerminalHyperlinkPaintListener { + return ApplicationScope.forApplicationScope() + .getOrCreate(TerminalHyperlinkPaintListener::class) { TerminalHyperlinkPaintListener() } + } } private val regex = Regex("https?://\\S*[^.\\s'\",()<>\\[\\]]") diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt index cdec50d..76e2585 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalPanel.kt @@ -1,5 +1,8 @@ package app.termora.terminal.panel +import app.termora.actions.DataProvider +import app.termora.actions.DataProviderSupport +import app.termora.actions.DataProviders import app.termora.terminal.* import com.formdev.flatlaf.util.SystemInfo import org.apache.commons.lang3.StringUtils @@ -27,7 +30,7 @@ import kotlin.time.Duration.Companion.milliseconds class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnector) : - JPanel(BorderLayout()) { + JPanel(BorderLayout()), DataProvider { companion object { val Debug = DataKey(Boolean::class) @@ -38,29 +41,13 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect private val terminalFindPanel = TerminalFindPanel(this, terminal) private val terminalDisplay = TerminalDisplay(this, terminal) val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal) + private val dataProviderSupport = DataProviderSupport() /** * 键盘事件 */ - private val actions = mutableListOf( - // 查找 - TerminalFindAction(this), - // 全选 - TerminalSelectAllAction(terminal), - // Zoom in - TerminalZoomInAction(), - // Zoom out - TerminalZoomOutAction(), - // Zoom reset - TerminalZoomResetAction(), - // 选择事件 - TerminalSelectionAction(terminal), - // 复制 - TerminalCopyAction(this), - // 粘贴 - TerminalPasteAction(this), - ) + private val actions = mutableListOf() /** @@ -130,6 +117,12 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect add(scrollBar, BorderLayout.EAST) hideFind() + + + // DataProviders + dataProviderSupport.addData(DataProviders.TerminalPanel, this) + dataProviderSupport.addData(DataProviders.Terminal, terminal) + dataProviderSupport.addData(DataProviders.PtyConnector, ptyConnector) } private fun initEvents() { @@ -474,4 +467,8 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect } } } + + override fun getData(dataKey: DataKey): T? { + return dataProviderSupport.getData(dataKey) + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt index aeab4f0..e9ee945 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt @@ -1,6 +1,11 @@ package app.termora.terminal.panel +import app.termora.actions.AnActionEvent +import app.termora.actions.TerminalCopyAction +import app.termora.actions.TerminalPasteAction import app.termora.terminal.* +import org.apache.commons.lang3.StringUtils +import org.jdesktop.swingx.action.ActionManager import org.slf4j.LoggerFactory import java.awt.Point import java.awt.event.KeyEvent @@ -210,10 +215,9 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane 'C' ) ) { - // copy - terminalPanel.getTerminalActions() - .filterIsInstance() - .forEach { it.actionPerformed(e) } + ActionManager.getInstance() + .getAction(TerminalCopyAction.COPY) + ?.actionPerformed(AnActionEvent(terminalPanel, StringUtils.EMPTY, e)) } private fun triggerPasteAction( @@ -226,10 +230,9 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane 'V' ) ) { - // paste - terminalPanel.getTerminalActions() - .filterIsInstance() - .forEach { it.actionPerformed(e) } + ActionManager.getInstance() + .getAction(TerminalPasteAction.PASTE) + ?.actionPerformed(AnActionEvent(terminalPanel, StringUtils.EMPTY, e)) } private fun selectWord(position: Position) { diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalPasteAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalPasteAction.kt deleted file mode 100644 index 9cddbc9..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalPasteAction.kt +++ /dev/null @@ -1,42 +0,0 @@ -package app.termora.terminal.panel - -import com.formdev.flatlaf.util.SystemInfo -import org.slf4j.LoggerFactory -import java.awt.datatransfer.DataFlavor -import java.awt.event.InputEvent -import java.awt.event.KeyEvent -import javax.swing.KeyStroke - -class TerminalPasteAction(private val terminalPanel: TerminalPanel) : TerminalPredicateAction { - companion object { - private val log = LoggerFactory.getLogger(TerminalPasteAction::class.java) - } - - private val systemClipboard get() = terminalPanel.toolkit.systemClipboard - - override fun actionPerformed(e: KeyEvent) { - 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) - } - } - } - } - - override fun test(keyStroke: KeyStroke, e: KeyEvent): Boolean { - if (SystemInfo.isMacOS) { - return KeyStroke.getKeyStroke(KeyEvent.VK_V, terminalPanel.toolkit.menuShortcutKeyMaskEx) == keyStroke - } - - // Shift + Insert - val keyStroke1 = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.SHIFT_DOWN_MASK) - // Ctrl + Shift + V - val keyStroke2 = KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK) - - return keyStroke == keyStroke1 || keyStroke == keyStroke2 - } - -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalSelectAllAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalSelectAllAction.kt deleted file mode 100644 index bb38d4f..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalSelectAllAction.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.termora.terminal.panel - -import app.termora.terminal.Position -import app.termora.terminal.Terminal -import java.awt.Toolkit -import java.awt.event.KeyEvent -import javax.swing.KeyStroke - -class TerminalSelectAllAction(private val terminal: Terminal) : TerminalAction( - KeyStroke.getKeyStroke( - KeyEvent.VK_A, - Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx - ) -) { - override fun actionPerformed(e: KeyEvent) { - terminal.getSelectionModel().setSelection( - Position(y = 1, x = 1), - Position(y = terminal.getDocument().getLineCount(), x = terminal.getTerminalModel().getCols()) - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalSelectionAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalSelectionAction.kt deleted file mode 100644 index f75b76e..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalSelectionAction.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.termora.terminal.panel - -import app.termora.terminal.Terminal -import java.awt.event.KeyEvent -import javax.swing.KeyStroke - -/** - * https://learn.microsoft.com/zh-cn/windows/terminal/selection - */ -class TerminalSelectionAction(private val terminal: Terminal) : TerminalPredicateAction { - - override fun actionPerformed(e: KeyEvent) { - } - - override fun test(keyStroke: KeyStroke, e: KeyEvent): Boolean { - return false - } - - -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalZoomAction.kt deleted file mode 100644 index 470f3ba..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomAction.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.termora.terminal.panel - -import app.termora.TerminalPanelFactory -import app.termora.db.Database -import java.awt.event.KeyEvent -import javax.swing.KeyStroke - -abstract class TerminalZoomAction(keyStroke: KeyStroke) : TerminalAction(keyStroke) { - protected val fontSize get() = Database.instance.terminal.fontSize - - override fun actionPerformed(e: KeyEvent) { - if (!zoom()) return - TerminalPanelFactory.instance.fireResize() - } - - abstract fun zoom(): Boolean -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomInAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalZoomInAction.kt deleted file mode 100644 index b94bc30..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomInAction.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.termora.terminal.panel - -import app.termora.db.Database -import org.apache.commons.lang3.SystemUtils -import java.awt.Toolkit -import java.awt.event.KeyEvent -import javax.swing.KeyStroke - -class TerminalZoomInAction : TerminalZoomAction( - KeyStroke.getKeyStroke( - if (SystemUtils.IS_OS_MAC_OSX) KeyEvent.VK_EQUALS else KeyEvent.VK_PLUS, - Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx - ) -) { - - override fun zoom(): Boolean { - Database.instance.terminal.fontSize += 2 - return true - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomOutAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalZoomOutAction.kt deleted file mode 100644 index e4e246f..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomOutAction.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.termora.terminal.panel - -import app.termora.db.Database -import java.awt.Toolkit -import java.awt.event.KeyEvent -import javax.swing.KeyStroke -import kotlin.math.max - -class TerminalZoomOutAction : TerminalZoomAction( - KeyStroke.getKeyStroke( - KeyEvent.VK_MINUS, - Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx - ) -) { - - override fun zoom(): Boolean { - val oldFontSize = fontSize - Database.instance.terminal.fontSize = max(fontSize - 2, 9) - return oldFontSize != fontSize - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomResetAction.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalZoomResetAction.kt deleted file mode 100644 index fa729dc..0000000 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalZoomResetAction.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.termora.terminal.panel - -import app.termora.db.Database -import java.awt.Toolkit -import java.awt.event.KeyEvent -import javax.swing.KeyStroke - -class TerminalZoomResetAction : TerminalZoomAction( - KeyStroke.getKeyStroke( - KeyEvent.VK_0, - Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx - ) -) { - - private val defaultFontSize = 16 - - override fun zoom(): Boolean { - if (fontSize == defaultFontSize) { - return false - } - Database.instance.terminal.fontSize = 16 - return true - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/tlog/LogViewerTerminalTab.kt b/src/main/kotlin/app/termora/tlog/LogViewerTerminalTab.kt index 31132d3..3ed5d7d 100644 --- a/src/main/kotlin/app/termora/tlog/LogViewerTerminalTab.kt +++ b/src/main/kotlin/app/termora/tlog/LogViewerTerminalTab.kt @@ -1,9 +1,6 @@ package app.termora.tlog -import app.termora.Host -import app.termora.Icons -import app.termora.Protocol -import app.termora.PtyHostTerminalTab +import app.termora.* import app.termora.terminal.PtyConnector import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -12,7 +9,8 @@ import java.io.FileNotFoundException import java.nio.file.Files import javax.swing.Icon -class LogViewerTerminalTab(private val file: File) : PtyHostTerminalTab( +class LogViewerTerminalTab(windowScope: WindowScope, private val file: File) : PtyHostTerminalTab( + windowScope, Host( name = file.name, protocol = Protocol.Local diff --git a/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt b/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt index e324288..42ae787 100644 --- a/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt +++ b/src/main/kotlin/app/termora/tlog/TerminalLoggerAction.kt @@ -1,13 +1,13 @@ package app.termora.tlog import app.termora.* -import app.termora.db.Database +import app.termora.actions.AnAction +import app.termora.actions.AnActionEvent import app.termora.native.FileChooser import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.util.SystemInfo import org.apache.commons.io.FileUtils import java.awt.Window -import java.awt.event.ActionEvent import java.io.File import java.time.LocalDate import javax.swing.JComponent @@ -15,7 +15,7 @@ import javax.swing.JFileChooser import javax.swing.SwingUtilities class TerminalLoggerAction : AnAction(I18n.getString("termora.terminal-logger"), Icons.listFiles) { - private val properties by lazy { Database.instance.properties } + private val properties by lazy { Database.getDatabase().properties } /** * 是否开启了记录 @@ -32,7 +32,7 @@ class TerminalLoggerAction : AnAction(I18n.getString("termora.terminal-logger"), smallIcon = if (isRecording) Icons.dotListFiles else Icons.listFiles } - override fun actionPerformed(evt: ActionEvent) { + override fun actionPerformed(evt: AnActionEvent) { val source = evt.source if (source !is JComponent) return @@ -91,9 +91,9 @@ class TerminalLoggerAction : AnAction(I18n.getString("termora.terminal-logger"), fc.showOpenDialog(owner).thenAccept { files -> if (files.isNotEmpty()) { SwingUtilities.invokeLater { - val manager = Application.getService(TerminalTabbedManager::class) + val manager = ApplicationScope.forWindowScope(owner).get(TerminalTabbedManager::class) for (file in files) { - val tab = LogViewerTerminalTab(file) + val tab = LogViewerTerminalTab(ApplicationScope.forWindowScope(owner), file) tab.start() manager.addTerminalTab(tab) } diff --git a/src/main/kotlin/app/termora/tlog/TerminalLoggerDataListener.kt b/src/main/kotlin/app/termora/tlog/TerminalLoggerDataListener.kt index 85f8f8d..39b285d 100644 --- a/src/main/kotlin/app/termora/tlog/TerminalLoggerDataListener.kt +++ b/src/main/kotlin/app/termora/tlog/TerminalLoggerDataListener.kt @@ -1,6 +1,7 @@ package app.termora.tlog import app.termora.* + import app.termora.terminal.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel diff --git a/src/main/kotlin/app/termora/transport/BookmarkButton.kt b/src/main/kotlin/app/termora/transport/BookmarkButton.kt index 756807e..78ae5d6 100644 --- a/src/main/kotlin/app/termora/transport/BookmarkButton.kt +++ b/src/main/kotlin/app/termora/transport/BookmarkButton.kt @@ -5,7 +5,7 @@ import app.termora.DynamicColor import app.termora.I18n import app.termora.Icons import app.termora.assertEventDispatchThread -import app.termora.db.Database +import app.termora.Database import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.ui.FlatUIUtils @@ -20,7 +20,7 @@ import javax.swing.SwingConstants import javax.swing.SwingUtilities class BookmarkButton : JButton(Icons.bookmarks) { - private val properties by lazy { Database.instance.properties } + private val properties by lazy { Database.getDatabase().properties } private val arrowWidth = 16 private val arrowSize = 6 diff --git a/src/main/kotlin/app/termora/transport/SFTPAction.kt b/src/main/kotlin/app/termora/transport/SFTPAction.kt index 34d0177..f59efc1 100644 --- a/src/main/kotlin/app/termora/transport/SFTPAction.kt +++ b/src/main/kotlin/app/termora/transport/SFTPAction.kt @@ -1,11 +1,14 @@ package app.termora.transport -import app.termora.* -import java.awt.event.ActionEvent +import app.termora.Icons +import app.termora.SFTPTerminalTab +import app.termora.actions.AnAction +import app.termora.actions.AnActionEvent +import app.termora.actions.DataProviders class SFTPAction : AnAction("SFTP", Icons.folder) { - override fun actionPerformed(evt: ActionEvent) { - val terminalTabbedManager = Application.getService(TerminalTabbedManager::class) + override fun actionPerformed(evt: AnActionEvent) { + val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return val tabs = terminalTabbedManager.getTerminalTabs() for (tab in tabs) { if (tab is SFTPTerminalTab) { diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 8bdc2ca..fba048e 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -88,6 +88,12 @@ termora.settings.about.issue=Issues termora.settings.about.third-party=Third Party termora.settings.about.termora=${termora.title} ({0}) is a cross-platform SSH client. +termora.settings.keymap=Keymap +termora.settings.keymap.shortcut=Shortcut +termora.settings.keymap.action=Action +termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}] + + termora.settings.restart.title=Restart termora.settings.restart.message=Changes will take effect after restarting the application @@ -268,9 +274,25 @@ termora.transport.jobs.table.estimated-time=Estimated time termora.transport.jobs.contextmenu.delete=${termora.remove} termora.transport.jobs.contextmenu.delete-all=Delete All + # ToolBar termora.toolbar.customize-toolbar=Customize Toolbar... + +# Actions +termora.actions.copy-from-terminal=Copy from Terminal +termora.actions.paste-to-terminal=Paste to Terminal +termora.actions.select-all-in-terminal=Select All in Terminal +termora.actions.open-terminal-find=Open Terminal Find +termora.actions.close-tab=Close Tab +termora.actions.zoom-in-terminal=Zoom In Terminal +termora.actions.zoom-out-terminal=Zoom Out Terminal +termora.actions.zoom-reset-terminal=Reset Terminal Zoom +termora.actions.open-local-terminal=Open Local Terminal +termora.actions.open-find-everywhere=Open FindEverywhere +termora.actions.open-new-window=Open new Window +termora.actions.switch-tab=Switch to specific Tab [1..9] + # Terminal termora.terminal.size=Size: {0} x {1} termora.terminal.copied=Copied diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index e6b42d2..3aa0487 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -92,6 +92,10 @@ termora.settings.about.issue=报告问题 termora.settings.about.third-party=第三方依赖 termora.settings.about.termora=${termora.title} ({0}) 是一个跨平台的 SSH 客户端。 +termora.settings.keymap=键盘 +termora.settings.keymap.shortcut=快捷键 +termora.settings.keymap.action=操作 +termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用 # Welcome termora.welcome.my-hosts=我的主机 @@ -260,6 +264,7 @@ termora.transport.jobs.table.speed=速度 termora.transport.jobs.table.estimated-time=剩余时间 termora.transport.jobs.contextmenu.delete-all=删除所有 + # ToolBar termora.toolbar.customize-toolbar=自定义工具栏... @@ -267,5 +272,19 @@ termora.terminal.size=大小: {0} x {1} termora.terminal.copied=已复制 +# Actions +termora.actions.copy-from-terminal=从终端复制 +termora.actions.paste-to-terminal=粘贴到终端 +termora.actions.select-all-in-terminal=在终端中全选 +termora.actions.open-terminal-find=打开终端查找 +termora.actions.close-tab=关闭标签页 +termora.actions.zoom-in-terminal=放大终端 +termora.actions.zoom-out-terminal=缩小终端 +termora.actions.zoom-reset-terminal=重置终端缩放 +termora.actions.open-local-terminal=打开本地终端 +termora.actions.open-find-everywhere=打开全局查找 +termora.actions.open-new-window=打开新窗口 +termora.actions.switch-tab=切换到特定标签页 [1..9] + # zmodem termora.addons.zmodem.skip=跳过 \ No newline at end of file diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 7b8d803..8f0d134 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -49,6 +49,12 @@ termora.setting.security.enter-password-again=請再輸入密碼 termora.setting.security.password-is-different=兩次密碼輸入不一致 termora.setting.security.mnemonic-note=請妥善保管助記詞,可用來忘記密碼時解鎖數據 +termora.settings.keymap=鍵盤 +termora.settings.keymap.shortcut=快捷鍵 +termora.settings.keymap.action=操作 +termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用 + + # Find everywhere termora.find-everywhere=尋找 termora.find-everywhere.search-for-something=搜尋點什麼 ... @@ -240,11 +246,27 @@ termora.transport.jobs.table.speed=速度 termora.transport.jobs.table.estimated-time=剩餘時間 termora.transport.jobs.contextmenu.delete-all=刪除所有 + # ToolBar termora.toolbar.customize-toolbar=自訂工具列... termora.terminal.size=大小: {0} x {1} termora.terminal.copied=已複製 +# Actions +termora.actions.copy-from-terminal=從終端複製 +termora.actions.paste-to-terminal=貼上到終端 +termora.actions.select-all-in-terminal=在終端中全選 +termora.actions.open-terminal-find=開啟終端搜尋 +termora.actions.close-tab=關閉分頁 +termora.actions.zoom-in-terminal=放大終端 +termora.actions.zoom-out-terminal=縮小終端 +termora.actions.zoom-reset-terminal=重置終端縮放 +termora.actions.open-local-terminal=開啟本地終端 +termora.actions.open-find-everywhere=開啟全域搜尋 +termora.actions.open-new-window=開啟新視窗 +termora.actions.switch-tab=切換到特定分頁 [1..9] + + # zmodem termora.addons.zmodem.skip=跳過 \ No newline at end of file diff --git a/src/main/resources/icons/copy.svg b/src/main/resources/icons/copy.svg new file mode 100644 index 0000000..100f530 --- /dev/null +++ b/src/main/resources/icons/copy.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/main/resources/icons/copy_dark.svg b/src/main/resources/icons/copy_dark.svg new file mode 100644 index 0000000..878b742 --- /dev/null +++ b/src/main/resources/icons/copy_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/main/resources/icons/delete.svg b/src/main/resources/icons/delete.svg new file mode 100644 index 0000000..8444cb0 --- /dev/null +++ b/src/main/resources/icons/delete.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/main/resources/icons/delete_dark.svg b/src/main/resources/icons/delete_dark.svg new file mode 100644 index 0000000..6a16028 --- /dev/null +++ b/src/main/resources/icons/delete_dark.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/main/resources/icons/fitContent.svg b/src/main/resources/icons/fitContent.svg new file mode 100644 index 0000000..3579008 --- /dev/null +++ b/src/main/resources/icons/fitContent.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/icons/fitContent_dark.svg b/src/main/resources/icons/fitContent_dark.svg new file mode 100644 index 0000000..01920a3 --- /dev/null +++ b/src/main/resources/icons/fitContent_dark.svg @@ -0,0 +1,8 @@ + + + + + + + +