mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
343d11482d | ||
|
|
7ef81a0116 | ||
|
|
5df62d5d3e | ||
|
|
7db650d69f | ||
|
|
8d80d38d63 | ||
|
|
48f05d4cff | ||
|
|
9a1cf387c0 | ||
|
|
8b7efefbdb | ||
|
|
75f21db325 | ||
|
|
b094c9d4ff | ||
|
|
0da3c95759 | ||
|
|
fa79473ece | ||
|
|
86ccb5e0cc | ||
|
|
f385f4b277 | ||
|
|
3d0ef2a331 | ||
|
|
96999205a8 | ||
|
|
ee7f3871eb | ||
|
|
df2e9b0743 | ||
|
|
7964950149 | ||
|
|
e2d77fe881 | ||
|
|
f5783c8587 | ||
|
|
346044b1ba | ||
|
|
aa6ec8dd43 | ||
|
|
e0e6a85a81 | ||
|
|
56ba107c87 | ||
|
|
0345848418 | ||
|
|
f1073fb53f | ||
|
|
ce1924c422 | ||
|
|
d6de0922c6 | ||
|
|
d5157d3a16 | ||
|
|
63b27a2f83 | ||
|
|
992015c8e5 | ||
|
|
5d459f9b0d | ||
|
|
88f20c4898 | ||
|
|
314c112d4b | ||
|
|
0cd818e9a0 | ||
|
|
0884486e91 | ||
|
|
e30316eab3 | ||
|
|
d321e766b1 | ||
|
|
6aaed92f2c | ||
|
|
21cf22906b | ||
|
|
1476368673 | ||
|
|
45ea822fd6 | ||
|
|
a71493e52c | ||
|
|
cb327f218c | ||
|
|
6881b6376f | ||
|
|
5027fd9dfb | ||
|
|
49cef39b8b | ||
|
|
5c4acf85e8 | ||
|
|
07bee64b7f | ||
|
|
923afb7e99 | ||
|
|
68df52bfc0 | ||
|
|
c2ee6fc8ac | ||
|
|
9d4562e7e3 | ||
|
|
5733b5f485 | ||
|
|
9dbdb5fd7a | ||
|
|
a1d1821553 | ||
|
|
4a8faea8c5 | ||
|
|
cfb841db00 | ||
|
|
a87d4ddf82 | ||
|
|
6071b251a4 | ||
|
|
950ff517bb | ||
|
|
70008978d8 | ||
|
|
7c445bdadb | ||
|
|
f24151f6d8 | ||
|
|
7d65a88d63 | ||
|
|
ed57c3e5b4 | ||
|
|
00f11c9ed5 | ||
|
|
5ebea06a95 | ||
|
|
3e5df2161b | ||
|
|
ffcb4d028e | ||
|
|
022ae402cc |
33
.github/workflows/linux-x86-64.yml
vendored
Normal file
33
.github/workflows/linux-x86-64.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Linux x86-64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-linux-x64-b509.30.tar.gz
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.5'
|
||||||
|
architecture: x64
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-x86-64
|
||||||
|
path: build/distributions/*.tar.gz
|
||||||
35
.github/workflows/osx-aarch64.yml
vendored
Normal file
35
.github/workflows/osx-aarch64.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: macOS aarch64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-aarch64-b509.30.tar.gz
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.5'
|
||||||
|
architecture: aarch64
|
||||||
|
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-osx-aarch64
|
||||||
|
path: build/distributions/*.dmg
|
||||||
34
.github/workflows/osx-x86-64.yml
vendored
Normal file
34
.github/workflows/osx-x86-64.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: macOS x86-64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-13
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.5-osx-x64-b509.30.tar.gz
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.5'
|
||||||
|
architecture: x64
|
||||||
|
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-osx-x86-64
|
||||||
|
path: build/distributions/*.dmg
|
||||||
29
.github/workflows/windows-x86-64.yml
vendored
Normal file
29
.github/workflows/windows-x86-64.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Windows x86-64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jetbrains'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
.\gradlew.bat dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-windows-x86-64
|
||||||
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
build/distributions/*.msi
|
||||||
56
README.md
56
README.md
@@ -1,46 +1,48 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
# Termora
|
# Termora
|
||||||
|
|
||||||
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/readme.png" alt="termora" />
|
<img src="./docs/readme.png" alt="termora" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
|
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
|
||||||
|
|
||||||
## 功能特性
|
## Features
|
||||||
|
|
||||||
- 支持 SSH 和本地终端
|
- SSH and local terminal support
|
||||||
- 支持 [SFTP](./docs/sftp-zh_CN.png) 文件传输
|
- [SFTP](./docs/sftp.png?raw=1) file transfer support
|
||||||
- 支持 Windows、macOS、Linux 平台
|
- Compatible with Windows, macOS, and Linux
|
||||||
- 支持 Zmodem 协议
|
- Zmodem protocol support
|
||||||
- 支持 SSH 端口转发
|
- SSH port forwarding
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com)
|
- Configuration synchronization via [Gist](https://gist.github.com)
|
||||||
- 支持宏(录制脚本并回放)
|
- Macro support (record and replay scripts)
|
||||||
- 支持关键词高亮
|
- Keyword highlighting
|
||||||
- 支持密钥管理器
|
- Key management
|
||||||
- 支持将命令发送到多个会话
|
- Broadcast commands to multiple sessions
|
||||||
- 支持 [Find Everywhere](./docs/findeverywhere.png) 快速跳转
|
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
||||||
- 支持数据加密
|
- Data encryption
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
## 下载
|
## Download
|
||||||
|
|
||||||
- [releases](https://github.com/TermoraDev/termora/releases/latest)
|
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
|
||||||
### macOS
|
## Development
|
||||||
|
|
||||||
由于苹果开发者证书正在申请中,所以 macOS 用户需要执行 `sudo xattr -r -d com.apple.quarantine /Applications/Termora.app` 后才可以运行程序。
|
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
|
||||||
|
|
||||||
## 开发
|
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
|
||||||
|
|
||||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
|
||||||
|
|
||||||
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
## LICENSE
|
||||||
|
|
||||||
## 协议
|
This software is distributed under a dual-license model. You may choose one of the following options:
|
||||||
|
|
||||||
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
||||||
|
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||||
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
|
||||||
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
|
||||||
|
|||||||
43
README.zh_CN.md
Normal file
43
README.zh_CN.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Termora
|
||||||
|
|
||||||
|
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="./docs/readme-zh_CN.png" alt="termora" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 支持 SSH 和本地终端
|
||||||
|
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
|
||||||
|
- 支持 Windows、macOS、Linux 平台
|
||||||
|
- 支持 Zmodem 协议
|
||||||
|
- 支持 SSH 端口转发
|
||||||
|
- 支持配置同步到 [Gist](https://gist.github.com)
|
||||||
|
- 支持宏(录制脚本并回放)
|
||||||
|
- 支持关键词高亮
|
||||||
|
- 支持密钥管理器
|
||||||
|
- 支持将命令发送到多个会话
|
||||||
|
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
||||||
|
- 支持数据加密
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## 下载
|
||||||
|
|
||||||
|
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
||||||
|
|
||||||
|
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
||||||
|
|
||||||
|
## 协议
|
||||||
|
|
||||||
|
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
||||||
|
|
||||||
|
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
||||||
|
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
||||||
12
THIRDPARTY
12
THIRDPARTY
@@ -46,6 +46,10 @@ flatlaf 3.5.4
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
|
flatlaf 3.5.4-no-natives
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf-extras 3.5.4
|
flatlaf-extras 3.5.4
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
@@ -229,3 +233,11 @@ https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|||||||
jediterm
|
jediterm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||||
|
|
||||||
|
mixpanel-java 1.5.3
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||||
|
|
||||||
|
json-20231013
|
||||||
|
Public Domain.
|
||||||
|
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||||
198
build.gradle.kts
198
build.gradle.kts
@@ -1,9 +1,9 @@
|
|||||||
import org.gradle.internal.jvm.Jvm
|
import org.gradle.internal.jvm.Jvm
|
||||||
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
|
||||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem
|
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
@@ -14,11 +14,20 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.1"
|
version = "1.0.5"
|
||||||
|
|
||||||
val os: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
var arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
|
|
||||||
|
// macOS 签名信息
|
||||||
|
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||||
|
val macOSSign = os.isMacOsX && macOSSignUsername.isNotBlank()
|
||||||
|
&& System.getenv("TERMORA_MAC_SIGN").toBoolean()
|
||||||
|
|
||||||
|
// macOS 公证信息
|
||||||
|
val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE") ?: StringUtils.EMPTY
|
||||||
|
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
|
||||||
|
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -27,17 +36,20 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// 由于签名和公证,macOS 不携带 natives
|
||||||
|
val useNoNativesFlatLaf = os.isMacOsX && macOSNotary && System.getenv("ENABLE_BUILD").toBoolean()
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(libs.hutool)
|
testImplementation(libs.hutool)
|
||||||
testImplementation(libs.sshj)
|
testImplementation(libs.sshj)
|
||||||
testImplementation(platform(libs.koin.bom))
|
|
||||||
testImplementation(libs.koin.core)
|
|
||||||
testImplementation(libs.jsch)
|
testImplementation(libs.jsch)
|
||||||
testImplementation(libs.rhino)
|
testImplementation(libs.rhino)
|
||||||
testImplementation(libs.delight.rhino.sandbox)
|
testImplementation(libs.delight.rhino.sandbox)
|
||||||
testImplementation(platform(libs.testcontainers.bom))
|
testImplementation(platform(libs.testcontainers.bom))
|
||||||
testImplementation(libs.testcontainers)
|
testImplementation(libs.testcontainers)
|
||||||
|
|
||||||
|
// implementation(platform(libs.koin.bom))
|
||||||
|
// implementation(libs.koin.core)
|
||||||
implementation(libs.slf4j.api)
|
implementation(libs.slf4j.api)
|
||||||
implementation(libs.pty4j)
|
implementation(libs.pty4j)
|
||||||
implementation(libs.slf4j.tinylog)
|
implementation(libs.slf4j.tinylog)
|
||||||
@@ -50,9 +62,25 @@ dependencies {
|
|||||||
implementation(libs.commons.compress)
|
implementation(libs.commons.compress)
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.flatlaf)
|
|
||||||
implementation(libs.flatlaf.extras)
|
implementation(libs.flatlaf) {
|
||||||
implementation(libs.flatlaf.swingx)
|
artifact {
|
||||||
|
if (useNoNativesFlatLaf) {
|
||||||
|
classifier = "no-natives"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
implementation(libs.flatlaf.extras) {
|
||||||
|
if (useNoNativesFlatLaf) {
|
||||||
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
implementation(libs.flatlaf.swingx) {
|
||||||
|
if (useNoNativesFlatLaf) {
|
||||||
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.swingx)
|
implementation(libs.swingx)
|
||||||
implementation(libs.jgoodies.forms)
|
implementation(libs.jgoodies.forms)
|
||||||
@@ -75,11 +103,18 @@ dependencies {
|
|||||||
implementation(libs.xodus.environment)
|
implementation(libs.xodus.environment)
|
||||||
implementation(libs.bip39)
|
implementation(libs.bip39)
|
||||||
implementation(libs.colorpicker)
|
implementation(libs.colorpicker)
|
||||||
|
implementation(libs.mixpanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
val args = mutableListOf(
|
val args = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
|
"-Xmx2g",
|
||||||
|
"-XX:+UseZGC",
|
||||||
|
"-XX:+ZUncommit",
|
||||||
|
"-XX:+ZGenerational",
|
||||||
|
"-XX:ZUncommitDelay=60",
|
||||||
|
"-XX:SoftMaxHeapSize=64m"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -103,8 +138,53 @@ tasks.test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("copy-dependencies") {
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
from(configurations.runtimeClasspath)
|
val dir = layout.buildDirectory.dir("libs")
|
||||||
.into("${layout.buildDirectory.get()}/libs")
|
from(configurations.runtimeClasspath).into(dir)
|
||||||
|
|
||||||
|
// 对 JNA 和 PTY4J 的本地库提取
|
||||||
|
// 提取出来是为了单独签名,不然无法通过公证
|
||||||
|
if (os.isMacOsX && macOSSign) {
|
||||||
|
doLast {
|
||||||
|
val jna = libs.jna.asProvider().get()
|
||||||
|
val dylib = dir.get().dir("dylib").asFile
|
||||||
|
val pty4j = libs.pty4j.get()
|
||||||
|
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||||
|
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = File(dylib, jna.name)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 删除所有二进制类库
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
||||||
|
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = FileUtils.getFile(dylib, pty4j.name, "darwin")
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 删除所有二进制类库
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对二进制签名
|
||||||
|
Files.walk(dylib.toPath()).use { paths ->
|
||||||
|
for (path in paths) {
|
||||||
|
if (Files.isRegularFile(path)) {
|
||||||
|
signMacOSLocalFile(path.toFile())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Exec>("jlink") {
|
tasks.register<Exec>("jlink") {
|
||||||
@@ -136,10 +216,16 @@ tasks.register<Exec>("jlink") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Exec>("jpackage") {
|
tasks.register<Exec>("jpackage") {
|
||||||
|
|
||||||
val buildDir = layout.buildDirectory.get()
|
val buildDir = layout.buildDirectory.get()
|
||||||
val options = mutableListOf(
|
val options = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
"-Xmx2g",
|
"-Xmx2g",
|
||||||
|
"-XX:+UseZGC",
|
||||||
|
"-XX:+ZUncommit",
|
||||||
|
"-XX:+ZGenerational",
|
||||||
|
"-XX:ZUncommitDelay=60",
|
||||||
|
"-XX:SoftMaxHeapSize=64m",
|
||||||
"-XX:+HeapDumpOnOutOfMemoryError",
|
"-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
"-Dlogger.console.level=off",
|
"-Dlogger.console.level=off",
|
||||||
"-Dkotlinx.coroutines.debug=off",
|
"-Dkotlinx.coroutines.debug=off",
|
||||||
@@ -164,6 +250,9 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--temp", "$buildDir/jpackage"))
|
arguments.addAll(listOf("--temp", "$buildDir/jpackage"))
|
||||||
arguments.addAll(listOf("--dest", "$buildDir/distributions"))
|
arguments.addAll(listOf("--dest", "$buildDir/distributions"))
|
||||||
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||||
|
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
||||||
|
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||||
|
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
||||||
|
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -193,6 +282,12 @@ tasks.register<Exec>("jpackage") {
|
|||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (os.isMacOsX && macOSSign) {
|
||||||
|
arguments.add("--mac-sign")
|
||||||
|
arguments.add("--mac-signing-key-user-name")
|
||||||
|
arguments.add(macOSSignUsername)
|
||||||
|
}
|
||||||
|
|
||||||
commandLine(arguments)
|
commandLine(arguments)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -207,12 +302,18 @@ tasks.register("dist") {
|
|||||||
|
|
||||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||||
|
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||||
|
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||||
|
val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath
|
||||||
|
|
||||||
// 清空目录
|
// 清空目录
|
||||||
exec { commandLine(gradlew, "clean") }
|
exec { commandLine(gradlew, "clean") }
|
||||||
|
|
||||||
// 打包并复制依赖
|
// 打包并复制依赖
|
||||||
exec { commandLine(gradlew, "jar", "copy-dependencies") }
|
exec {
|
||||||
|
commandLine(gradlew, "jar", "copy-dependencies")
|
||||||
|
environment("ENABLE_BUILD" to true)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查依赖的开源协议
|
// 检查依赖的开源协议
|
||||||
exec { commandLine(gradlew, "check-license") }
|
exec { commandLine(gradlew, "check-license") }
|
||||||
@@ -224,30 +325,73 @@ tasks.register("dist") {
|
|||||||
exec { commandLine(gradlew, "jpackage") }
|
exec { commandLine(gradlew, "jpackage") }
|
||||||
|
|
||||||
// pack
|
// pack
|
||||||
|
if (os.isWindows) { // zip and msi
|
||||||
|
// zip
|
||||||
exec {
|
exec {
|
||||||
if (os.isWindows) { // zip
|
|
||||||
commandLine(
|
commandLine(
|
||||||
"tar", "-vacf",
|
"tar", "-vacf",
|
||||||
distributionDir.file("${project.name}-${project.version}-windows-${arch.name}.zip").asFile.absolutePath,
|
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||||
project.name.uppercaseFirstChar()
|
project.name.uppercaseFirstChar()
|
||||||
)
|
)
|
||||||
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// msi
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"cmd", "/c", "move",
|
||||||
|
"${project.name.uppercaseFirstChar()}-${project.version}.msi",
|
||||||
|
"${finalFilenameWithoutExtension}.msi"
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
} else if (os.isLinux) { // tar.gz
|
} else if (os.isLinux) { // tar.gz
|
||||||
|
exec {
|
||||||
commandLine(
|
commandLine(
|
||||||
"tar", "-czvf",
|
"tar", "-czvf",
|
||||||
distributionDir.file("${project.name}-${project.version}-linux-${arch.name}.tar.gz").asFile.absolutePath,
|
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||||
project.name.uppercaseFirstChar()
|
project.name.uppercaseFirstChar()
|
||||||
)
|
)
|
||||||
workingDir = distributionDir.asFile
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
} else if (os.isMacOsX) { // rename
|
} else if (os.isMacOsX) { // rename
|
||||||
|
exec {
|
||||||
commandLine(
|
commandLine(
|
||||||
"mv",
|
"mv",
|
||||||
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
||||||
distributionDir.file("${project.name}-${project.version}-osx-${arch.name}.dmg").asFile.absolutePath,
|
macOSFinalFilePath,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw GradleException("${os.name} is not supported")
|
throw GradleException("${os.name} is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// sign dmg
|
||||||
|
if (os.isMacOsX && macOSSign) {
|
||||||
|
|
||||||
|
// sign
|
||||||
|
signMacOSLocalFile(File(macOSFinalFilePath))
|
||||||
|
|
||||||
|
// notary
|
||||||
|
if (macOSNotary) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun", "notarytool",
|
||||||
|
"submit", macOSFinalFilePath,
|
||||||
|
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||||
|
"--wait",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定公证信息
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
"stapler", "staple", macOSFinalFilePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +430,26 @@ tasks.register("check-license") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* macOS 对本地文件进行签名
|
||||||
|
*/
|
||||||
|
fun signMacOSLocalFile(file: File) {
|
||||||
|
if (os.isMacOsX && macOSSign) {
|
||||||
|
if (file.exists() && file.isFile) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/codesign",
|
||||||
|
"-s", macOSSignUsername,
|
||||||
|
"--timestamp", "--force",
|
||||||
|
"-vvvv", "--options", "runtime",
|
||||||
|
file.absolutePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain {
|
jvmToolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
|||||||
BIN
docs/findeverywhere-zh_CN.png
Normal file
BIN
docs/findeverywhere-zh_CN.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 61 KiB |
BIN
docs/readme-zh_CN.png
Normal file
BIN
docs/readme-zh_CN.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/readme.png
BIN
docs/readme.png
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 96 KiB |
@@ -1,3 +1,4 @@
|
|||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
kotlin.daemon.jvmargs=-Xmx4g
|
||||||
@@ -40,6 +40,7 @@ colorpicker = "2.0.1"
|
|||||||
rhino = "1.7.15"
|
rhino = "1.7.15"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.20.4"
|
testcontainers = "1.20.4"
|
||||||
|
mixpanel = "1.5.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
@@ -95,6 +96,7 @@ bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39"
|
|||||||
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||||
|
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
433
src/main/java/app/termora/SwingUtils.java
Normal file
433
src/main/java/app/termora/SwingUtils.java
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
package app.termora;/*
|
||||||
|
* @(#)SwingUtils.java 1.02 11/15/08
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
//package darrylbu.util;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of utility methods for Swing.
|
||||||
|
*
|
||||||
|
* @author Darryl Burke
|
||||||
|
*/
|
||||||
|
public final class SwingUtils {
|
||||||
|
|
||||||
|
private SwingUtils() {
|
||||||
|
throw new Error("SwingUtils is just a container for static methods");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for searching below <code>container</code> in the
|
||||||
|
* component hierarchy and return nested components that are instances of
|
||||||
|
* class <code>clazz</code> it finds. Returns an empty list if no such
|
||||||
|
* components exist in the container.
|
||||||
|
* <P>
|
||||||
|
* Invoking this method with a class parameter of JComponent.class
|
||||||
|
* will return all nested components.
|
||||||
|
* <P>
|
||||||
|
* This method invokes getDescendantsOfType(clazz, container, true)
|
||||||
|
*
|
||||||
|
* @param clazz the class of components whose instances are to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @return the List of components
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> List<T> getDescendantsOfType(
|
||||||
|
Class<T> clazz, Container container) {
|
||||||
|
return getDescendantsOfType(clazz, container, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for searching below <code>container</code> in the
|
||||||
|
* component hierarchy and return nested components that are instances of
|
||||||
|
* class <code>clazz</code> it finds. Returns an empty list if no such
|
||||||
|
* components exist in the container.
|
||||||
|
* <P>
|
||||||
|
* Invoking this method with a class parameter of JComponent.class
|
||||||
|
* will return all nested components.
|
||||||
|
*
|
||||||
|
* @param clazz the class of components whose instances are to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @param nested true to list components nested within another listed
|
||||||
|
* component, false otherwise
|
||||||
|
* @return the List of components
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> List<T> getDescendantsOfType(
|
||||||
|
Class<T> clazz, Container container, boolean nested) {
|
||||||
|
List<T> tList = new ArrayList<T>();
|
||||||
|
for (Component component : container.getComponents()) {
|
||||||
|
if (clazz.isAssignableFrom(component.getClass())) {
|
||||||
|
tList.add(clazz.cast(component));
|
||||||
|
}
|
||||||
|
if (nested || !clazz.isAssignableFrom(component.getClass())) {
|
||||||
|
tList.addAll(SwingUtils.<T>getDescendantsOfType(clazz,
|
||||||
|
(Container) component, nested));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that searches below <code>container</code> in the
|
||||||
|
* component hierarchy and returns the first found component that is an
|
||||||
|
* instance of class <code>clazz</code> having the bound property value.
|
||||||
|
* Returns {@code null} if such component cannot be found.
|
||||||
|
* <P>
|
||||||
|
* This method invokes getDescendantOfType(clazz, container, property, value,
|
||||||
|
* true)
|
||||||
|
*
|
||||||
|
* @param clazz the class of component whose instance is to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @param property the className of the bound property, exactly as expressed in
|
||||||
|
* the accessor e.g. "Text" for getText(), "Value" for getValue().
|
||||||
|
* @param value the value of the bound property
|
||||||
|
* @return the component, or null if no such component exists in the
|
||||||
|
* container
|
||||||
|
* @throws java.lang.IllegalArgumentException if the bound property does
|
||||||
|
* not exist for the class or cannot be accessed
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> T getDescendantOfType(
|
||||||
|
Class<T> clazz, Container container, String property, Object value)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
return getDescendantOfType(clazz, container, property, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that searches below <code>container</code> in the
|
||||||
|
* component hierarchy and returns the first found component that is an
|
||||||
|
* instance of class <code>clazz</code> and has the bound property value.
|
||||||
|
* Returns {@code null} if such component cannot be found.
|
||||||
|
*
|
||||||
|
* @param clazz the class of component whose instance to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @param property the className of the bound property, exactly as expressed in
|
||||||
|
* the accessor e.g. "Text" for getText(), "Value" for getValue().
|
||||||
|
* @param value the value of the bound property
|
||||||
|
* @param nested true to list components nested within another component
|
||||||
|
* which is also an instance of <code>clazz</code>, false otherwise
|
||||||
|
* @return the component, or null if no such component exists in the
|
||||||
|
* container
|
||||||
|
* @throws java.lang.IllegalArgumentException if the bound property does
|
||||||
|
* not exist for the class or cannot be accessed
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> T getDescendantOfType(Class<T> clazz,
|
||||||
|
Container container, String property, Object value, boolean nested)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
List<T> list = getDescendantsOfType(clazz, container, nested);
|
||||||
|
return getComponentFromList(clazz, list, property, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for searching below <code>container</code> in the
|
||||||
|
* component hierarchy and return nested components of class
|
||||||
|
* <code>clazz</code> it finds. Returns an empty list if no such
|
||||||
|
* components exist in the container.
|
||||||
|
* <P>
|
||||||
|
* This method invokes getDescendantsOfClass(clazz, container, true)
|
||||||
|
*
|
||||||
|
* @param clazz the class of components to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @return the List of components
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> List<T> getDescendantsOfClass(
|
||||||
|
Class<T> clazz, Container container) {
|
||||||
|
return getDescendantsOfClass(clazz, container, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for searching below <code>container</code> in the
|
||||||
|
* component hierarchy and return nested components of class
|
||||||
|
* <code>clazz</code> it finds. Returns an empty list if no such
|
||||||
|
* components exist in the container.
|
||||||
|
*
|
||||||
|
* @param clazz the class of components to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @param nested true to list components nested within another listed
|
||||||
|
* component, false otherwise
|
||||||
|
* @return the List of components
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> List<T> getDescendantsOfClass(
|
||||||
|
Class<T> clazz, Container container, boolean nested) {
|
||||||
|
List<T> tList = new ArrayList<T>();
|
||||||
|
for (Component component : container.getComponents()) {
|
||||||
|
if (clazz.equals(component.getClass())) {
|
||||||
|
tList.add(clazz.cast(component));
|
||||||
|
}
|
||||||
|
if (nested || !clazz.equals(component.getClass())) {
|
||||||
|
tList.addAll(SwingUtils.<T>getDescendantsOfClass(clazz,
|
||||||
|
(Container) component, nested));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that searches below <code>container</code> in the
|
||||||
|
* component hierarchy in a depth first manner and returns the first
|
||||||
|
* found component of class <code>clazz</code> having the bound property
|
||||||
|
* value.
|
||||||
|
* <P>
|
||||||
|
* Returns {@code null} if such component cannot be found.
|
||||||
|
* <P>
|
||||||
|
* This method invokes getDescendantOfClass(clazz, container, property,
|
||||||
|
* value, true)
|
||||||
|
*
|
||||||
|
* @param clazz the class of component to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @param property the className of the bound property, exactly as expressed in
|
||||||
|
* the accessor e.g. "Text" for getText(), "Value" for getValue().
|
||||||
|
* This parameter is case sensitive.
|
||||||
|
* @param value the value of the bound property
|
||||||
|
* @return the component, or null if no such component exists in the
|
||||||
|
* container's hierarchy.
|
||||||
|
* @throws java.lang.IllegalArgumentException if the bound property does
|
||||||
|
* not exist for the class or cannot be accessed
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
|
||||||
|
Container container, String property, Object value)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
return getDescendantOfClass(clazz, container, property, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method that searches below <code>container</code> in the
|
||||||
|
* component hierarchy in a depth first manner and returns the first
|
||||||
|
* found component of class <code>clazz</code> having the bound property
|
||||||
|
* value.
|
||||||
|
* <P>
|
||||||
|
* Returns {@code null} if such component cannot be found.
|
||||||
|
*
|
||||||
|
* @param clazz the class of component to be found.
|
||||||
|
* @param container the container at which to begin the search
|
||||||
|
* @param property the className of the bound property, exactly as expressed
|
||||||
|
* in the accessor e.g. "Text" for getText(), "Value" for getValue().
|
||||||
|
* This parameter is case sensitive.
|
||||||
|
* @param value the value of the bound property
|
||||||
|
* @param nested true to include components nested within another listed
|
||||||
|
* component, false otherwise
|
||||||
|
* @return the component, or null if no such component exists in the
|
||||||
|
* container's hierarchy
|
||||||
|
* @throws java.lang.IllegalArgumentException if the bound property does
|
||||||
|
* not exist for the class or cannot be accessed
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
|
||||||
|
Container container, String property, Object value, boolean nested)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
List<T> list = getDescendantsOfClass(clazz, container, nested);
|
||||||
|
return getComponentFromList(clazz, list, property, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends JComponent> T getComponentFromList(Class<T> clazz,
|
||||||
|
List<T> list, String property, Object value)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
T retVal = null;
|
||||||
|
Method method = null;
|
||||||
|
try {
|
||||||
|
method = clazz.getMethod("get" + property);
|
||||||
|
} catch (NoSuchMethodException ex) {
|
||||||
|
try {
|
||||||
|
method = clazz.getMethod("is" + property);
|
||||||
|
} catch (NoSuchMethodException ex1) {
|
||||||
|
throw new IllegalArgumentException("Property " + property +
|
||||||
|
" not found in class " + clazz.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (T t : list) {
|
||||||
|
Object testVal = method.invoke(t);
|
||||||
|
if (equals(value, testVal)) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InvocationTargetException ex) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Error accessing property " + property +
|
||||||
|
" in class " + clazz.getName());
|
||||||
|
} catch (IllegalAccessException ex) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Property " + property +
|
||||||
|
" cannot be accessed in class " + clazz.getName());
|
||||||
|
} catch (SecurityException ex) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Property " + property +
|
||||||
|
" cannot be accessed in class " + clazz.getName());
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for determining whether two objects are either
|
||||||
|
* equal or both null.
|
||||||
|
*
|
||||||
|
* @param obj1 the first reference object to compare.
|
||||||
|
* @param obj2 the second reference object to compare.
|
||||||
|
* @return true if obj1 and obj2 are equal or if both are null,
|
||||||
|
* false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean equals(Object obj1, Object obj2) {
|
||||||
|
return obj1 == null ? obj2 == null : obj1.equals(obj2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for mapping a container in the hierarchy to its
|
||||||
|
* contained components. The keys are the containers, and the values
|
||||||
|
* are lists of contained components.
|
||||||
|
* <P>
|
||||||
|
* Implementation note: The returned value is a HashMap and the values
|
||||||
|
* are of type ArrayList. This is subject to change, so callers should
|
||||||
|
* code against the interfaces Map and List.
|
||||||
|
*
|
||||||
|
* @param container The JComponent to be mapped
|
||||||
|
* @param nested true to drill down to nested containers, false otherwise
|
||||||
|
* @return the Map of the UI
|
||||||
|
*/
|
||||||
|
public static Map<JComponent, List<JComponent>> getComponentMap(
|
||||||
|
JComponent container, boolean nested) {
|
||||||
|
HashMap<JComponent, List<JComponent>> retVal =
|
||||||
|
new HashMap<JComponent, List<JComponent>>();
|
||||||
|
for (JComponent component : getDescendantsOfType(JComponent.class,
|
||||||
|
container, false)) {
|
||||||
|
if (!retVal.containsKey(container)) {
|
||||||
|
retVal.put(container,
|
||||||
|
new ArrayList<JComponent>());
|
||||||
|
}
|
||||||
|
retVal.get(container).add(component);
|
||||||
|
if (nested) {
|
||||||
|
retVal.putAll(getComponentMap(component, nested));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for retrieving a subset of the UIDefaults pertaining
|
||||||
|
* to a particular class.
|
||||||
|
*
|
||||||
|
* @param clazz the class of interest
|
||||||
|
* @return the UIDefaults of the class
|
||||||
|
*/
|
||||||
|
public static UIDefaults getUIDefaultsOfClass(Class clazz) {
|
||||||
|
String name = clazz.getName();
|
||||||
|
name = name.substring(name.lastIndexOf(".") + 2);
|
||||||
|
return getUIDefaultsOfClass(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for retrieving a subset of the UIDefaults pertaining
|
||||||
|
* to a particular class.
|
||||||
|
*
|
||||||
|
* @param className fully qualified name of the class of interest
|
||||||
|
* @return the UIDefaults of the class named
|
||||||
|
*/
|
||||||
|
public static UIDefaults getUIDefaultsOfClass(String className) {
|
||||||
|
UIDefaults retVal = new UIDefaults();
|
||||||
|
UIDefaults defaults = UIManager.getLookAndFeelDefaults();
|
||||||
|
List<?> listKeys = Collections.list(defaults.keys());
|
||||||
|
for (Object key : listKeys) {
|
||||||
|
if (key instanceof String && ((String) key).startsWith(className)) {
|
||||||
|
String stringKey = (String) key;
|
||||||
|
String property = stringKey;
|
||||||
|
if (stringKey.contains(".")) {
|
||||||
|
property = stringKey.substring(stringKey.indexOf(".") + 1);
|
||||||
|
}
|
||||||
|
retVal.put(property, defaults.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for retrieving the UIDefault for a single property
|
||||||
|
* of a particular class.
|
||||||
|
*
|
||||||
|
* @param clazz the class of interest
|
||||||
|
* @param property the property to query
|
||||||
|
* @return the UIDefault property, or null if not found
|
||||||
|
*/
|
||||||
|
public static Object getUIDefaultOfClass(Class clazz, String property) {
|
||||||
|
Object retVal = null;
|
||||||
|
UIDefaults defaults = getUIDefaultsOfClass(clazz);
|
||||||
|
List<Object> listKeys = Collections.list(defaults.keys());
|
||||||
|
for (Object key : listKeys) {
|
||||||
|
if (key.equals(property)) {
|
||||||
|
return defaults.get(key);
|
||||||
|
}
|
||||||
|
if (key.toString().equalsIgnoreCase(property)) {
|
||||||
|
retVal = defaults.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude methods that return values that are meaningless to the user
|
||||||
|
*/
|
||||||
|
static Set<String> setExclude = new HashSet<String>();
|
||||||
|
static {
|
||||||
|
setExclude.add("getFocusCycleRootAncestor");
|
||||||
|
setExclude.add("getAccessibleContext");
|
||||||
|
setExclude.add("getColorModel");
|
||||||
|
setExclude.add("getGraphics");
|
||||||
|
setExclude.add("getGraphicsConfiguration");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for obtaining most non-null human readable properties
|
||||||
|
* of a JComponent. Array properties are not included.
|
||||||
|
* <P>
|
||||||
|
* Implementation note: The returned value is a HashMap. This is subject
|
||||||
|
* to change, so callers should code against the interface Map.
|
||||||
|
*
|
||||||
|
* @param component the component whose proerties are to be determined
|
||||||
|
* @return the class and value of the properties
|
||||||
|
*/
|
||||||
|
public static Map<Object, Object> getProperties(JComponent component) {
|
||||||
|
Map<Object, Object> retVal = new HashMap<Object, Object>();
|
||||||
|
Class<?> clazz = component.getClass();
|
||||||
|
Method[] methods = clazz.getMethods();
|
||||||
|
Object value = null;
|
||||||
|
for (Method method : methods) {
|
||||||
|
if (method.getName().matches("^(is|get).*") &&
|
||||||
|
method.getParameterTypes().length == 0) {
|
||||||
|
try {
|
||||||
|
Class returnType = method.getReturnType();
|
||||||
|
if (returnType != void.class &&
|
||||||
|
!returnType.getName().startsWith("[") &&
|
||||||
|
!setExclude.contains(method.getName())) {
|
||||||
|
String key = method.getName();
|
||||||
|
value = method.invoke(component);
|
||||||
|
if (value != null && !(value instanceof Component)) {
|
||||||
|
retVal.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ignore exceptions that arise if the property could not be accessed
|
||||||
|
} catch (IllegalAccessException ex) {
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
} catch (InvocationTargetException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to obtain the Swing class from which this
|
||||||
|
* component was directly or indirectly derived.
|
||||||
|
*
|
||||||
|
* @param component The component whose Swing superclass is to be
|
||||||
|
* determined
|
||||||
|
* @return The nearest Swing class in the inheritance tree
|
||||||
|
*/
|
||||||
|
public static <T extends JComponent> Class getJClass(T component) {
|
||||||
|
Class<?> clazz = component.getClass();
|
||||||
|
while (!clazz.getName().matches("javax.swing.J[^.]*$")) {
|
||||||
|
clazz = clazz.getSuperclass();
|
||||||
|
}
|
||||||
|
return clazz;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,25 +2,16 @@ package app.termora
|
|||||||
|
|
||||||
object Actions {
|
object Actions {
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开设置
|
|
||||||
*/
|
|
||||||
const val SETTING = "SettingAction"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将命令发送到多个会话
|
* 将命令发送到多个会话
|
||||||
*/
|
*/
|
||||||
const val MULTIPLE = "MultipleAction"
|
const val MULTIPLE = "MultipleAction"
|
||||||
|
|
||||||
/**
|
|
||||||
* 查找
|
|
||||||
*/
|
|
||||||
const val FIND_EVERYWHERE = "FindEverywhereAction"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关键词高亮
|
* 关键词高亮
|
||||||
*/
|
*/
|
||||||
const val KEYWORD_HIGHLIGHT_EVERYWHERE = "KeywordHighlightAction"
|
const val KEYWORD_HIGHLIGHT = "KeywordHighlightAction"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key manager
|
* Key manager
|
||||||
@@ -38,13 +29,15 @@ object Actions {
|
|||||||
*/
|
*/
|
||||||
const val MACRO = "MacroAction"
|
const val MACRO = "MacroAction"
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加主机对话框
|
|
||||||
*/
|
|
||||||
const val ADD_HOST = "AddHostAction"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开一个主机
|
* 终端日志记录
|
||||||
*/
|
*/
|
||||||
const val OPEN_HOST = "OpenHostAction"
|
const val TERMINAL_LOGGER = "TerminalLogAction"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开 SFTP Tab Action
|
||||||
|
*/
|
||||||
|
const val SFTP = "SFTPAction"
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import org.jdesktop.swingx.action.BoundAction
|
|
||||||
import javax.swing.Icon
|
|
||||||
|
|
||||||
abstract class AnAction : BoundAction {
|
|
||||||
|
|
||||||
constructor() : super()
|
|
||||||
constructor(icon: Icon) : super() {
|
|
||||||
super.putValue(SMALL_ICON, icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(name: String?) : super(name)
|
|
||||||
constructor(name: String?, icon: Icon?) : super(name, icon)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -16,14 +16,11 @@ import java.awt.Desktop
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
|
|
||||||
object Application {
|
object Application {
|
||||||
private val services = Collections.synchronizedMap(mutableMapOf<KClass<*>, Any>())
|
|
||||||
private lateinit var baseDataDir: File
|
private lateinit var baseDataDir: File
|
||||||
|
|
||||||
|
|
||||||
@@ -125,22 +122,6 @@ object Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : Any> getService(clazz: KClass<T>): T {
|
|
||||||
if (services.containsKey(clazz)) {
|
|
||||||
return services[clazz] as T
|
|
||||||
}
|
|
||||||
throw IllegalStateException("$clazz does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun registerService(clazz: KClass<*>, service: Any) {
|
|
||||||
if (services.containsKey(clazz)) {
|
|
||||||
throw IllegalStateException("$clazz already registered")
|
|
||||||
}
|
|
||||||
services[clazz] = service
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryBrowse(uri: URI) {
|
private fun tryBrowse(uri: URI) {
|
||||||
if (SystemInfo.isWindows) {
|
if (SystemInfo.isWindows) {
|
||||||
ProcessBuilder("explorer", uri.toString()).start()
|
ProcessBuilder("explorer", uri.toString()).start()
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将在 JVM 进程退出时释放
|
|
||||||
*/
|
|
||||||
class ApplicationDisposable : Disposable {
|
|
||||||
companion object {
|
|
||||||
val instance by lazy { ApplicationDisposable() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,102 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
import app.termora.actions.ActionManager
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatSystemProperties
|
import com.formdev.flatlaf.FlatSystemProperties
|
||||||
import com.formdev.flatlaf.extras.FlatInspector
|
import com.formdev.flatlaf.extras.FlatInspector
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
import com.sun.jna.platform.WindowUtils
|
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||||
import com.sun.jna.platform.win32.User32
|
import com.mixpanel.mixpanelapi.MessageBuilder
|
||||||
import com.sun.jna.ptr.IntByReference
|
import com.mixpanel.mixpanelapi.MixpanelAPI
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.LocaleUtils
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.math.NumberUtils
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.tinylog.configuration.Configuration
|
import org.tinylog.configuration.Configuration
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.RandomAccessFile
|
|
||||||
import java.nio.channels.FileChannel
|
import java.nio.channels.FileChannel
|
||||||
import java.nio.channels.FileLock
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.file.Paths
|
||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class ApplicationRunner {
|
class ApplicationRunner {
|
||||||
|
private lateinit var singletonChannel: FileChannel
|
||||||
private lateinit var singletonLock: FileLock
|
private lateinit var singletonLock: FileLock
|
||||||
private val log by lazy {
|
private val log by lazy {
|
||||||
if (!::singletonLock.isInitialized) {
|
if (!::singletonLock.isInitialized) {
|
||||||
throw UnsupportedOperationException("Singleton lock is not initialized")
|
throw UnsupportedOperationException("Singleton lock is not initialized")
|
||||||
}
|
}
|
||||||
LoggerFactory.getLogger("Main")
|
LoggerFactory.getLogger(ApplicationRunner::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun run() {
|
fun run() {
|
||||||
|
measureTimeMillis {
|
||||||
// 覆盖 tinylog 配置
|
// 覆盖 tinylog 配置
|
||||||
setupTinylog()
|
val setupTinylog = measureTimeMillis { setupTinylog() }
|
||||||
|
|
||||||
// 是否单例
|
// 是否单例
|
||||||
checkSingleton()
|
val checkSingleton = measureTimeMillis { checkSingleton() }
|
||||||
|
|
||||||
// 打印系统信息
|
// 打印系统信息
|
||||||
printSystemInfo()
|
val printSystemInfo = measureTimeMillis { printSystemInfo() }
|
||||||
|
|
||||||
SwingUtilities.invokeAndWait {
|
|
||||||
// 打开数据库
|
// 打开数据库
|
||||||
openDatabase()
|
val openDatabase = measureTimeMillis { openDatabase() }
|
||||||
|
|
||||||
// 加载设置
|
// 加载设置
|
||||||
loadSettings()
|
val loadSettings = measureTimeMillis { loadSettings() }
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
val enableAnalytics = measureTimeMillis { enableAnalytics() }
|
||||||
|
|
||||||
|
// init ActionManager、KeymapManager
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
ActionManager.getInstance()
|
||||||
|
KeymapManager.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
// 设置 LAF
|
// 设置 LAF
|
||||||
setupLaf()
|
val setupLaf = measureTimeMillis { setupLaf() }
|
||||||
|
|
||||||
// 解密数据
|
// 解密数据
|
||||||
openDoor()
|
val openDoor = measureTimeMillis { openDoor() }
|
||||||
|
|
||||||
// 启动主窗口
|
// 启动主窗口
|
||||||
startMainFrame()
|
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("setupTinylog: {}ms", setupTinylog)
|
||||||
|
log.debug("checkSingleton: {}ms", checkSingleton)
|
||||||
|
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
||||||
|
log.debug("openDatabase: {}ms", openDatabase)
|
||||||
|
log.debug("loadSettings: {}ms", loadSettings)
|
||||||
|
log.debug("enableAnalytics: {}ms", enableAnalytics)
|
||||||
|
log.debug("setupLaf: {}ms", setupLaf)
|
||||||
|
log.debug("openDoor: {}ms", openDoor)
|
||||||
|
log.debug("startMainFrame: {}ms", startMainFrame)
|
||||||
|
}
|
||||||
|
}.let {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("run: {}ms", it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun openDoor() {
|
private fun openDoor() {
|
||||||
if (Doorman.instance.isWorking()) {
|
if (Doorman.getInstance().isWorking()) {
|
||||||
if (!DoormanDialog(null).open()) {
|
if (!DoormanDialog(null).open()) {
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
@@ -72,17 +104,11 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startMainFrame() {
|
private fun startMainFrame() {
|
||||||
val frame = TermoraFrame()
|
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
|
||||||
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
|
|
||||||
frame.setSize(1280, 800)
|
|
||||||
frame.setLocationRelativeTo(null)
|
|
||||||
frame.isVisible = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
val language = Database.instance.appearance.language
|
val language = Database.getDatabase().appearance.language
|
||||||
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
|
val locale = runCatching { LocaleUtils.toLocale(language) }.getOrElse { Locale.getDefault() }
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("Language: {} , Locale: {}", language, locale)
|
log.info("Language: {} , Locale: {}", language, locale)
|
||||||
@@ -101,21 +127,21 @@ class ApplicationRunner {
|
|||||||
JDialog.setDefaultLookAndFeelDecorated(true)
|
JDialog.setDefaultLookAndFeelDecorated(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
val themeManager = ThemeManager.instance
|
val themeManager = ThemeManager.getInstance()
|
||||||
val settings = Database.instance
|
val appearance = Database.getDatabase().appearance
|
||||||
var theme = settings.appearance.theme
|
var theme = appearance.theme
|
||||||
|
// 如果是跟随系统
|
||||||
// 如果是跟随系统或者不存在样式,那么使用默认的
|
if (appearance.followSystem) {
|
||||||
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) {
|
|
||||||
theme = if (OsThemeDetector.getDetector().isDark) {
|
theme = if (OsThemeDetector.getDetector().isDark) {
|
||||||
"Dark"
|
appearance.darkTheme
|
||||||
} else {
|
} else {
|
||||||
"Light"
|
appearance.lightTheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
themeManager.change(theme, true)
|
themeManager.change(theme, true)
|
||||||
|
|
||||||
|
if (Application.isUnknownVersion())
|
||||||
FlatInspector.install("ctrl shift alt X");
|
FlatInspector.install("ctrl shift alt X");
|
||||||
|
|
||||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
@@ -164,21 +190,21 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun printSystemInfo() {
|
private fun printSystemInfo() {
|
||||||
if (log.isInfoEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.info("Welcome to ${Application.getName()} ${Application.getVersion()}!")
|
log.debug("Welcome to ${Application.getName()} ${Application.getVersion()}!")
|
||||||
log.info(
|
log.debug(
|
||||||
"JVM name: {} , vendor: {} , version: {}",
|
"JVM name: {} , vendor: {} , version: {}",
|
||||||
SystemUtils.JAVA_VM_NAME,
|
SystemUtils.JAVA_VM_NAME,
|
||||||
SystemUtils.JAVA_VM_VENDOR,
|
SystemUtils.JAVA_VM_VENDOR,
|
||||||
SystemUtils.JAVA_VM_VERSION,
|
SystemUtils.JAVA_VM_VERSION,
|
||||||
)
|
)
|
||||||
log.info(
|
log.debug(
|
||||||
"OS name: {} , version: {} , arch: {}",
|
"OS name: {} , version: {} , arch: {}",
|
||||||
SystemUtils.OS_NAME,
|
SystemUtils.OS_NAME,
|
||||||
SystemUtils.OS_VERSION,
|
SystemUtils.OS_VERSION,
|
||||||
SystemUtils.OS_ARCH
|
SystemUtils.OS_ARCH
|
||||||
)
|
)
|
||||||
log.info("Base config dir: ${Application.getBaseDataDir().absolutePath}")
|
log.debug("Base config dir: ${Application.getBaseDataDir().absolutePath}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,36 +223,14 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
|
|
||||||
private fun checkSingleton() {
|
private fun checkSingleton() {
|
||||||
val file = File(Application.getBaseDataDir(), "lock")
|
singletonChannel = FileChannel.open(
|
||||||
val pidFile = File(Application.getBaseDataDir(), "pid")
|
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
val raf = RandomAccessFile(file, "rw")
|
)
|
||||||
val lock = raf.channel.tryLock()
|
|
||||||
|
|
||||||
if (lock != null) {
|
|
||||||
pidFile.writeText(ProcessHandle.current().pid().toString())
|
|
||||||
pidFile.deleteOnExit()
|
|
||||||
file.deleteOnExit()
|
|
||||||
} else {
|
|
||||||
if (SystemInfo.isWindows && pidFile.exists()) {
|
|
||||||
val pid = NumberUtils.toLong(pidFile.readText())
|
|
||||||
for (window in WindowUtils.getAllWindows(false)) {
|
|
||||||
if (pid > 0) {
|
|
||||||
val processId = IntByReference()
|
|
||||||
User32.INSTANCE.GetWindowThreadProcessId(window.hwnd, processId)
|
|
||||||
if (processId.value.toLong() != pid) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if (window.title != Application.getName() || window.filePath.endsWith("explorer.exe")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
User32.INSTANCE.ShowWindow(window.hwnd, User32.SW_SHOWNOACTIVATE)
|
|
||||||
User32.INSTANCE.SetForegroundWindow(window.hwnd)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val lock = singletonChannel.tryLock()
|
||||||
|
if (lock == null) {
|
||||||
System.err.println("Program is already running")
|
System.err.println("Program is already running")
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
@@ -236,9 +240,8 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
|
|
||||||
private fun openDatabase() {
|
private fun openDatabase() {
|
||||||
val dir = Application.getDatabaseFile()
|
|
||||||
try {
|
try {
|
||||||
Database.open(dir)
|
Database.getDatabase()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
@@ -251,4 +254,48 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计 https://mixpanel.com
|
||||||
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
private fun enableAnalytics() {
|
||||||
|
if (Application.isUnknownVersion()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val properties = JSONObject()
|
||||||
|
properties.put("os", SystemUtils.OS_NAME)
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
properties.put("platform", "Linux")
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
properties.put("platform", "Windows")
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
properties.put("platform", "macOS")
|
||||||
|
}
|
||||||
|
properties.put("version", Application.getVersion())
|
||||||
|
properties.put("language", Locale.getDefault().toString())
|
||||||
|
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
|
||||||
|
.event(getAnalyticsUserID(), "launch", properties)
|
||||||
|
val delivery = ClientDelivery()
|
||||||
|
delivery.addMessage(message)
|
||||||
|
MixpanelAPI().deliver(delivery, true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAnalyticsUserID(): String {
|
||||||
|
var id = Database.getDatabase().properties.getString("AnalyticsUserID")
|
||||||
|
if (id.isNullOrBlank()) {
|
||||||
|
id = UUID.randomUUID().toSimpleString()
|
||||||
|
Database.getDatabase().properties.putString("AnalyticsUserID", id)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
374
src/main/kotlin/app/termora/CustomizeToolBarDialog.kt
Normal file
374
src/main/kotlin/app/termora/CustomizeToolBarDialog.kt
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.event.ListDataEvent
|
||||||
|
import javax.swing.event.ListDataListener
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class CustomizeToolBarDialog(
|
||||||
|
owner: Window,
|
||||||
|
private val toolbar: TermoraToolBar
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
|
private val moveTopBtn = JButton(Icons.moveUp)
|
||||||
|
private val moveBottomBtn = JButton(Icons.moveDown)
|
||||||
|
private val upBtn = JButton(Icons.up)
|
||||||
|
private val downBtn = JButton(Icons.down)
|
||||||
|
|
||||||
|
private val leftBtn = JButton(Icons.left)
|
||||||
|
private val rightBtn = JButton(Icons.right)
|
||||||
|
private val resetBtn = JButton(Icons.refresh)
|
||||||
|
private val allToLeftBtn = JButton(Icons.applyNotConflictsRight)
|
||||||
|
private val allToRightBtn = JButton(Icons.applyNotConflictsLeft)
|
||||||
|
|
||||||
|
private val leftList = ToolBarActionList()
|
||||||
|
private val rightList = ToolBarActionList()
|
||||||
|
private val actionManager get() = ActionManager.getInstance()
|
||||||
|
|
||||||
|
private var isOk = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
||||||
|
isModal = true
|
||||||
|
controlsVisible = false
|
||||||
|
isResizable = false
|
||||||
|
title = I18n.getString("termora.toolbar.customize-toolbar")
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
moveTopBtn.isEnabled = false
|
||||||
|
moveBottomBtn.isEnabled = false
|
||||||
|
downBtn.isEnabled = false
|
||||||
|
upBtn.isEnabled = false
|
||||||
|
|
||||||
|
leftBtn.isEnabled = false
|
||||||
|
rightBtn.isEnabled = false
|
||||||
|
|
||||||
|
initEvents()
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
|
||||||
|
allToLeftBtn.isEnabled = !rightList.model.isEmpty
|
||||||
|
allToRightBtn.isEnabled = !leftList.model.isEmpty
|
||||||
|
|
||||||
|
val box = JToolBar(JToolBar.VERTICAL)
|
||||||
|
box.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||||
|
box.add(rightBtn)
|
||||||
|
box.add(leftBtn)
|
||||||
|
box.add(Box.createVerticalGlue())
|
||||||
|
box.add(resetBtn)
|
||||||
|
box.add(Box.createVerticalGlue())
|
||||||
|
box.add(allToRightBtn)
|
||||||
|
box.add(allToLeftBtn)
|
||||||
|
box.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||||
|
|
||||||
|
val box2 = JToolBar(JToolBar.VERTICAL)
|
||||||
|
box2.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||||
|
box2.add(moveTopBtn)
|
||||||
|
box2.add(upBtn)
|
||||||
|
box2.add(Box.createVerticalGlue())
|
||||||
|
box2.add(downBtn)
|
||||||
|
box2.add(moveBottomBtn)
|
||||||
|
box2.add(Box.createVerticalStrut(leftList.fixedCellHeight))
|
||||||
|
|
||||||
|
|
||||||
|
return FormBuilder.create().debug(false)
|
||||||
|
.border(BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor))
|
||||||
|
.layout(FormLayout("default:grow, pref, default:grow, pref", "fill:p:grow"))
|
||||||
|
.add(JScrollPane(leftList).apply {
|
||||||
|
border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
|
||||||
|
}).xy(1, 1)
|
||||||
|
.add(box).xy(2, 1)
|
||||||
|
.add(JScrollPane(rightList).apply {
|
||||||
|
border = BorderFactory.createMatteBorder(0, 1, 0, 1, DynamicColor.BorderColor)
|
||||||
|
}).xy(3, 1)
|
||||||
|
.add(box2).xy(4, 1)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
rightList.addListSelectionListener { resetMoveButtons() }
|
||||||
|
|
||||||
|
leftList.addListSelectionListener {
|
||||||
|
val indices = leftList.selectedIndices
|
||||||
|
rightBtn.isEnabled = indices.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
leftList.model.addListDataListener(object : ListDataListener {
|
||||||
|
override fun intervalAdded(e: ListDataEvent) {
|
||||||
|
contentsChanged(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intervalRemoved(e: ListDataEvent) {
|
||||||
|
contentsChanged(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentsChanged(e: ListDataEvent) {
|
||||||
|
allToLeftBtn.isEnabled = !rightList.model.isEmpty
|
||||||
|
allToRightBtn.isEnabled = !leftList.model.isEmpty
|
||||||
|
resetMoveButtons()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
rightList.model.addListDataListener(object : ListDataListener {
|
||||||
|
override fun intervalAdded(e: ListDataEvent) {
|
||||||
|
contentsChanged(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intervalRemoved(e: ListDataEvent) {
|
||||||
|
contentsChanged(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentsChanged(e: ListDataEvent) {
|
||||||
|
allToLeftBtn.isEnabled = !rightList.model.isEmpty
|
||||||
|
allToRightBtn.isEnabled = !leftList.model.isEmpty
|
||||||
|
resetMoveButtons()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resetBtn.addActionListener {
|
||||||
|
leftList.model.removeAllElements()
|
||||||
|
rightList.model.removeAllElements()
|
||||||
|
for (action in toolbar.getAllActions()) {
|
||||||
|
actionManager.getAction(action.id)?.let {
|
||||||
|
rightList.model.addElement(ActionHolder(action.id, it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move first
|
||||||
|
moveTopBtn.addActionListener {
|
||||||
|
val indices = rightList.selectedIndices.sortedDescending()
|
||||||
|
rightList.clearSelection()
|
||||||
|
for (index in indices.indices) {
|
||||||
|
val ele = rightList.model.getElementAt(indices[index])
|
||||||
|
rightList.model.removeElementAt(indices[index])
|
||||||
|
rightList.model.add(index, ele)
|
||||||
|
rightList.selectionModel.addSelectionInterval(index, max(index - 1, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move up
|
||||||
|
upBtn.addActionListener {
|
||||||
|
val indices = rightList.selectedIndices.sortedDescending()
|
||||||
|
rightList.clearSelection()
|
||||||
|
for (index in indices) {
|
||||||
|
val ele = rightList.model.getElementAt(index)
|
||||||
|
rightList.model.removeElementAt(index)
|
||||||
|
rightList.model.add(index - 1, ele)
|
||||||
|
rightList.selectionModel.addSelectionInterval(max(index - 1, 0), max(index - 1, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move down
|
||||||
|
downBtn.addActionListener {
|
||||||
|
val indices = rightList.selectedIndices.sortedDescending()
|
||||||
|
rightList.clearSelection()
|
||||||
|
for (index in indices) {
|
||||||
|
val ele = rightList.model.getElementAt(index)
|
||||||
|
rightList.model.removeElementAt(index)
|
||||||
|
rightList.model.add(index + 1, ele)
|
||||||
|
rightList.selectionModel.addSelectionInterval(index + 1, index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move last
|
||||||
|
moveBottomBtn.addActionListener {
|
||||||
|
val indices = rightList.selectedIndices.sortedDescending()
|
||||||
|
val size = rightList.model.size
|
||||||
|
rightList.clearSelection()
|
||||||
|
for (index in indices.indices) {
|
||||||
|
val ele = rightList.model.getElementAt(indices[index])
|
||||||
|
rightList.model.removeElementAt(indices[index])
|
||||||
|
rightList.model.add(size - index - 1, ele)
|
||||||
|
rightList.selectionModel.addSelectionInterval(size - index - 1, size - index - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allToLeftBtn.addActionListener {
|
||||||
|
while (!rightList.model.isEmpty) {
|
||||||
|
val ele = rightList.model.getElementAt(0)
|
||||||
|
rightList.model.removeElementAt(0)
|
||||||
|
leftList.model.addElement(ele)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allToRightBtn.addActionListener {
|
||||||
|
while (!leftList.model.isEmpty) {
|
||||||
|
val ele = leftList.model.getElementAt(0)
|
||||||
|
leftList.model.removeElementAt(0)
|
||||||
|
rightList.model.addElement(ele)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leftBtn.addActionListener {
|
||||||
|
val indices = rightList.selectedIndices.sortedDescending()
|
||||||
|
for (index in indices) {
|
||||||
|
val ele = rightList.model.getElementAt(index)
|
||||||
|
rightList.model.removeElementAt(index)
|
||||||
|
leftList.model.addElement(ele)
|
||||||
|
}
|
||||||
|
rightList.clearSelection()
|
||||||
|
val index = min(indices.max(), rightList.model.size - 1)
|
||||||
|
if (!rightList.model.isEmpty) {
|
||||||
|
rightList.addSelectionInterval(index, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rightBtn.addActionListener {
|
||||||
|
val indices = leftList.selectedIndices.sortedDescending()
|
||||||
|
val rightSelectedIndex = if (rightList.selectedIndices.isEmpty()) rightList.model.size else
|
||||||
|
rightList.selectionModel.maxSelectionIndex + 1
|
||||||
|
|
||||||
|
if (indices.isNotEmpty()) {
|
||||||
|
for (index in indices.indices) {
|
||||||
|
val ele = leftList.model.getElementAt(indices[index])
|
||||||
|
leftList.model.removeElementAt(indices[index])
|
||||||
|
rightList.model.add(rightSelectedIndex + index, ele)
|
||||||
|
}
|
||||||
|
|
||||||
|
leftList.clearSelection()
|
||||||
|
val index = min(indices.max(), leftList.model.size - 1)
|
||||||
|
if (!leftList.model.isEmpty) {
|
||||||
|
leftList.addSelectionInterval(index, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
rightList.clearSelection()
|
||||||
|
rightList.addSelectionInterval(rightSelectedIndex, rightSelectedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
removeWindowListener(this)
|
||||||
|
|
||||||
|
|
||||||
|
for (action in toolbar.getActions()) {
|
||||||
|
if (action.visible) {
|
||||||
|
actionManager.getAction(action.id)
|
||||||
|
?.let { rightList.model.addElement(ActionHolder(action.id, it)) }
|
||||||
|
} else {
|
||||||
|
actionManager.getAction(action.id)
|
||||||
|
?.let { leftList.model.addElement(ActionHolder(action.id, it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetMoveButtons() {
|
||||||
|
val indices = rightList.selectedIndices
|
||||||
|
if (indices.isEmpty()) {
|
||||||
|
moveTopBtn.isEnabled = false
|
||||||
|
moveBottomBtn.isEnabled = false
|
||||||
|
downBtn.isEnabled = false
|
||||||
|
upBtn.isEnabled = false
|
||||||
|
} else {
|
||||||
|
moveTopBtn.isEnabled = !indices.contains(0)
|
||||||
|
upBtn.isEnabled = moveTopBtn.isEnabled
|
||||||
|
moveBottomBtn.isEnabled = !indices.contains(rightList.model.size - 1)
|
||||||
|
downBtn.isEnabled = moveBottomBtn.isEnabled
|
||||||
|
}
|
||||||
|
leftBtn.isEnabled = indices.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ToolBarActionList : JList<ActionHolder>() {
|
||||||
|
private val model = DefaultListModel<ActionHolder>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
setModel(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
background = UIManager.getColor("window")
|
||||||
|
fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||||
|
cellRenderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: StringUtils.EMPTY
|
||||||
|
if (value is ActionHolder) {
|
||||||
|
val action = value.action
|
||||||
|
text = action.getValue(Action.NAME)?.toString() ?: text
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
if (value is ActionHolder) {
|
||||||
|
val action = value.action
|
||||||
|
val icon = action.getValue(Action.SMALL_ICON) as Icon?
|
||||||
|
if (icon != null) {
|
||||||
|
this.icon = icon
|
||||||
|
if (icon is DynamicIcon) {
|
||||||
|
if (isSelected && cellHasFocus) {
|
||||||
|
this.icon = icon.dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModel(): DefaultListModel<ActionHolder> {
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
isOk = true
|
||||||
|
|
||||||
|
val actions = mutableListOf<ToolBarAction>()
|
||||||
|
for (i in 0 until rightList.model.size()) {
|
||||||
|
actions.add(ToolBarAction(rightList.model.getElementAt(i).id, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until leftList.model.size()) {
|
||||||
|
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
Database.getDatabase()
|
||||||
|
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun open(): Boolean {
|
||||||
|
isModal = true
|
||||||
|
isVisible = true
|
||||||
|
return isOk
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ActionHolder(val id: String, val action: Action)
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package app.termora.db
|
package app.termora
|
||||||
|
|
||||||
import app.termora.*
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.highlight.KeywordHighlight
|
import app.termora.highlight.KeywordHighlight
|
||||||
|
import app.termora.keymap.Keymap
|
||||||
import app.termora.keymgr.OhKeyPair
|
import app.termora.keymgr.OhKeyPair
|
||||||
import app.termora.macro.Macro
|
import app.termora.macro.Macro
|
||||||
import app.termora.sync.SyncType
|
import app.termora.sync.SyncType
|
||||||
@@ -26,24 +26,15 @@ import kotlin.time.Duration.Companion.minutes
|
|||||||
|
|
||||||
class Database private constructor(private val env: Environment) : Disposable {
|
class Database private constructor(private val env: Environment) : Disposable {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val KEYMAP_STORE = "Keymap"
|
||||||
private const val HOST_STORE = "Host"
|
private const val HOST_STORE = "Host"
|
||||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||||
private const val MACRO_STORE = "Macro"
|
private const val MACRO_STORE = "Macro"
|
||||||
private const val KEY_PAIR_STORE = "KeyPair"
|
private const val KEY_PAIR_STORE = "KeyPair"
|
||||||
private val log = LoggerFactory.getLogger(Database::class.java)
|
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||||
private lateinit var database: Database
|
|
||||||
|
|
||||||
val instance by lazy {
|
|
||||||
if (!::database.isInitialized) {
|
|
||||||
throw UnsupportedOperationException("Database has not been initialized!")
|
|
||||||
}
|
|
||||||
database
|
|
||||||
}
|
|
||||||
|
|
||||||
fun open(dir: File) {
|
private fun open(dir: File): Database {
|
||||||
if (::database.isInitialized) {
|
|
||||||
throw UnsupportedOperationException("Database is already open")
|
|
||||||
}
|
|
||||||
val config = EnvironmentConfig()
|
val config = EnvironmentConfig()
|
||||||
// 32MB
|
// 32MB
|
||||||
config.setLogFileSize(1024 * 32)
|
config.setLogFileSize(1024 * 32)
|
||||||
@@ -51,8 +42,12 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
// 5m
|
// 5m
|
||||||
config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt())
|
config.setGcStartIn(5.minutes.inWholeMilliseconds.toInt())
|
||||||
val environment = Environments.newInstance(dir, config)
|
val environment = Environments.newInstance(dir, config)
|
||||||
database = Database(environment)
|
return Database(environment)
|
||||||
Disposer.register(ApplicationDisposable.instance, database)
|
}
|
||||||
|
|
||||||
|
fun getDatabase(): Database {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(Database::class) { open(Application.getDatabaseFile()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +57,41 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
val appearance by lazy { Appearance() }
|
val appearance by lazy { Appearance() }
|
||||||
val sync by lazy { Sync() }
|
val sync by lazy { Sync() }
|
||||||
|
|
||||||
private val doorman get() = Doorman.instance
|
private val doorman get() = Doorman.getInstance()
|
||||||
|
|
||||||
|
|
||||||
|
fun getKeymaps(): Collection<Keymap> {
|
||||||
|
val array = env.computeInTransaction { tx ->
|
||||||
|
openCursor<String>(tx, KEYMAP_STORE) { _, value ->
|
||||||
|
value
|
||||||
|
}.values
|
||||||
|
}
|
||||||
|
|
||||||
|
val keymaps = mutableListOf<Keymap>()
|
||||||
|
for (text in array.iterator()) {
|
||||||
|
keymaps.add(Keymap.fromJSON(text) ?: continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keymaps
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addKeymap(keymap: Keymap) {
|
||||||
|
env.executeInTransaction {
|
||||||
|
put(it, KEYMAP_STORE, keymap.name, keymap.toJSON())
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Added Keymap: ${keymap.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeKeymap(name: String) {
|
||||||
|
env.executeInTransaction {
|
||||||
|
delete(it, KEYMAP_STORE, name)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Removed Keymap: $name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getHosts(): Collection<Host> {
|
fun getHosts(): Collection<Host> {
|
||||||
@@ -413,7 +442,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
/**
|
/**
|
||||||
* 字体大小
|
* 字体大小
|
||||||
*/
|
*/
|
||||||
var fontSize by IntPropertyDelegate(16)
|
var fontSize by IntPropertyDelegate(14)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 最大行数
|
* 最大行数
|
||||||
@@ -459,7 +488,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 安全的通用属性
|
* 安全的通用属性
|
||||||
*/
|
*/
|
||||||
open inner class SafetyProperties(name: String) : Property(name) {
|
open inner class SafetyProperties(name: String) : Property(name) {
|
||||||
private val doorman get() = Doorman.instance
|
private val doorman get() = Doorman.getInstance()
|
||||||
|
|
||||||
public override fun getString(key: String): String? {
|
public override fun getString(key: String): String? {
|
||||||
var value = super.getString(key)
|
var value = super.getString(key)
|
||||||
@@ -522,6 +551,8 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 跟随系统
|
* 跟随系统
|
||||||
*/
|
*/
|
||||||
var followSystem by BooleanPropertyDelegate(true)
|
var followSystem by BooleanPropertyDelegate(true)
|
||||||
|
var darkTheme by StringPropertyDelegate("Dark")
|
||||||
|
var lightTheme by StringPropertyDelegate("Light")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* 语言
|
||||||
@@ -548,6 +579,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||||
var rangeMacros by BooleanPropertyDelegate(true)
|
var rangeMacros by BooleanPropertyDelegate(true)
|
||||||
|
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token
|
* Token
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import java.awt.BorderLayout
|
import java.awt.*
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.Window
|
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
|
|
||||||
abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
||||||
private val rootPanel = JPanel(BorderLayout())
|
private val rootPanel = JPanel(BorderLayout())
|
||||||
private val titleLabel = JLabel()
|
private val titleLabel = JLabel()
|
||||||
@@ -21,6 +21,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_ACTION = "DEFAULT_ACTION"
|
const val DEFAULT_ACTION = "DEFAULT_ACTION"
|
||||||
|
private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -38,9 +39,21 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
|
|
||||||
protected var lostFocusDispose = false
|
protected var lostFocusDispose = false
|
||||||
protected var escapeDispose = true
|
protected var escapeDispose = true
|
||||||
|
var processGlobalKeymap: Boolean
|
||||||
|
get() {
|
||||||
|
val v = super.rootPane.getClientProperty(PROCESS_GLOBAL_KEYMAP)
|
||||||
|
if (v is Boolean) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
protected set(value) {
|
||||||
|
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
|
||||||
|
}
|
||||||
|
|
||||||
protected fun init() {
|
protected fun init() {
|
||||||
|
|
||||||
|
|
||||||
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
||||||
|
|
||||||
initTitleBar()
|
initTitleBar()
|
||||||
@@ -132,7 +145,32 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "close")
|
||||||
|
|
||||||
rootPane.actionMap.put("close", object : AnAction() {
|
rootPane.actionMap.put("close", object : AnAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
|
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
|
||||||
|
JPopupMenu::class.java,
|
||||||
|
c as Container, true
|
||||||
|
)
|
||||||
|
|
||||||
|
var openPopup = false
|
||||||
|
for (p in popups) {
|
||||||
|
p.isVisible = false
|
||||||
|
openPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val window = SwingUtilities.windowForComponent(c)
|
||||||
|
val windows = window.ownedWindows
|
||||||
|
for (w in windows) {
|
||||||
|
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
|
||||||
|
openPopup = true
|
||||||
|
w.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openPopup) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
doCancelAction()
|
doCancelAction()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -154,12 +192,12 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
if (SystemInfo.isWindows) {
|
if (SystemInfo.isWindows) {
|
||||||
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
|
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
|
||||||
override fun windowClosed(e: WindowEvent) {
|
override fun windowClosed(e: WindowEvent) {
|
||||||
ThemeManager.instance.removeThemeChangeListener(this)
|
ThemeManager.getInstance().removeThemeChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun windowOpened(e: WindowEvent) {
|
override fun windowOpened(e: WindowEvent) {
|
||||||
onChanged()
|
onChanged()
|
||||||
ThemeManager.instance.addThemeChangeListener(this)
|
ThemeManager.getInstance().addThemeChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
@@ -190,7 +228,8 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
putValue(DEFAULT_ACTION, true)
|
putValue(DEFAULT_ACTION, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
doOKAction()
|
doOKAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +237,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
|
|
||||||
protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) {
|
protected inner class CancelAction : AnAction(I18n.getString("termora.cancel")) {
|
||||||
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
doCancelAction()
|
doCancelAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
import app.termora.AES.encodeBase64String
|
import app.termora.AES.encodeBase64String
|
||||||
import app.termora.db.Database
|
|
||||||
|
|
||||||
class PasswordWrongException : RuntimeException()
|
class PasswordWrongException : RuntimeException()
|
||||||
|
|
||||||
class Doorman private constructor() {
|
class Doorman private constructor() : Disposable {
|
||||||
private val properties get() = Database.instance.properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
private var key = byteArrayOf()
|
private var key = byteArrayOf()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { Doorman() }
|
fun getInstance(): Doorman {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(Doorman::class) { Doorman() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isWorking(): Boolean {
|
fun isWorking(): Boolean {
|
||||||
@@ -82,4 +83,8 @@ class Doorman private constructor() {
|
|||||||
checkIsWorking()
|
checkIsWorking()
|
||||||
return key.contentEquals(convertKey(password))
|
return key.contentEquals(convertKey(password))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
key = byteArrayOf()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
import app.termora.db.Database
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
@@ -17,7 +18,6 @@ import org.slf4j.LoggerFactory
|
|||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import java.awt.event.KeyAdapter
|
import java.awt.event.KeyAdapter
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
@@ -95,7 +95,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
.add(safeBtn).xy(4, rows).apply { rows += step }
|
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||||
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||||
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val option = OptionPane.showConfirmDialog(
|
val option = OptionPane.showConfirmDialog(
|
||||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||||
options = arrayOf(
|
options = arrayOf(
|
||||||
@@ -130,10 +130,11 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val keyBackup = Database.instance.properties.getString("doorman-key-backup")
|
val keyBackup = Database.getDatabase()
|
||||||
|
.properties.getString("doorman-key-backup")
|
||||||
?: throw IllegalStateException("doorman-key-backup is null")
|
?: throw IllegalStateException("doorman-key-backup is null")
|
||||||
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
||||||
Doorman.instance.work(key)
|
Doorman.getInstance().work(key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||||
@@ -157,7 +158,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Doorman.instance.work(passwordTextField.password)
|
Doorman.getInstance().work(passwordTextField.password)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is PasswordWrongException) {
|
if (e is PasswordWrongException) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.keymgr.KeyManager
|
|
||||||
import app.termora.keymgr.OhKeyPair
|
|
||||||
|
|
||||||
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||||
init {
|
init {
|
||||||
generalOption.portTextField.value = host.port
|
generalOption.portTextField.value = host.port
|
||||||
@@ -10,15 +7,12 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
generalOption.protocolTypeComboBox.selectedItem = host.protocol
|
generalOption.protocolTypeComboBox.selectedItem = host.protocol
|
||||||
generalOption.usernameTextField.text = host.username
|
generalOption.usernameTextField.text = host.username
|
||||||
generalOption.hostTextField.text = host.host
|
generalOption.hostTextField.text = host.host
|
||||||
generalOption.passwordTextField.text = host.authentication.password
|
|
||||||
generalOption.remarkTextArea.text = host.remark
|
generalOption.remarkTextArea.text = host.remark
|
||||||
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
|
generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type
|
||||||
if (host.authentication.type == AuthenticationType.PublicKey) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
val ohKeyPair = KeyManager.instance.getOhKeyPair(host.authentication.password)
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
if (ohKeyPair != null) {
|
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
generalOption.publicKeyTextField.text = ohKeyPair.name
|
generalOption.publicKeyComboBox.selectedItem = host.authentication.password
|
||||||
generalOption.publicKeyTextField.putClientProperty(OhKeyPair::class, ohKeyPair)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
@@ -34,6 +28,15 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
terminalOption.heartbeatIntervalTextField.value = host.options.heartbeatInterval
|
||||||
|
|
||||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||||
|
|
||||||
|
if (host.options.jumpHosts.isNotEmpty()) {
|
||||||
|
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||||
|
for (id in host.options.jumpHosts) {
|
||||||
|
jumpHostsOption.jumpHosts.add(hosts[id] ?: continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpHostsOption.filter = { it.id != host.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHost(): Host {
|
override fun getHost(): Host {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||||
@@ -40,25 +44,33 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
private fun createTestConnectionAction(): AbstractAction {
|
private fun createTestConnectionAction(): AbstractAction {
|
||||||
return object : AnAction(I18n.getString("termora.new-host.test-connection")) {
|
return object : AnAction(I18n.getString("termora.new-host.test-connection")) {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (!pane.validateFields()) {
|
if (!pane.validateFields()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
putValue(NAME, "${I18n.getString("termora.new-host.test-connection")}...")
|
||||||
SwingUtilities.invokeLater {
|
isEnabled = false
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
testConnection(pane.getHost())
|
testConnection(pane.getHost())
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
putValue(NAME, I18n.getString("termora.new-host.test-connection"))
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun testConnection(host: Host) {
|
private suspend fun testConnection(host: Host) {
|
||||||
|
val owner = this
|
||||||
if (host.protocol != Protocol.SSH) {
|
if (host.protocol != Protocol.SSH) {
|
||||||
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful"))
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(owner, I18n.getString("termora.new-host.test-connection-successful"))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +78,21 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
|||||||
var session: ClientSession? = null
|
var session: ClientSession? = null
|
||||||
try {
|
try {
|
||||||
client = SshClients.openClient(host)
|
client = SshClients.openClient(host)
|
||||||
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
session = SshClients.openSession(host, client)
|
session = SshClients.openSession(host, client)
|
||||||
OptionPane.showMessageDialog(this, I18n.getString("termora.new-host.test-connection-successful"))
|
withContext(Dispatchers.Swing) {
|
||||||
} catch (e: Exception) {
|
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this, ExceptionUtils.getRootCauseMessage(e),
|
owner,
|
||||||
|
I18n.getString("termora.new-host.test-connection-successful")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
session?.close()
|
session?.close()
|
||||||
client?.close()
|
client?.close()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface HostListener : EventListener {
|
interface HostListener : EventListener {
|
||||||
@@ -12,10 +11,12 @@ interface HostListener : EventListener {
|
|||||||
|
|
||||||
class HostManager private constructor() {
|
class HostManager private constructor() {
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { HostManager() }
|
fun getInstance(): HostManager {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(HostManager::class) { HostManager() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val database get() = Database.instance
|
private val database get() = Database.getDatabase()
|
||||||
private val listeners = mutableListOf<HostListener>()
|
private val listeners = mutableListOf<HostListener>()
|
||||||
|
|
||||||
fun addHost(host: Host, notify: Boolean = true) {
|
fun addHost(host: Host, notify: Boolean = true) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
import app.termora.keymgr.OhKeyPair
|
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
@@ -12,6 +12,7 @@ import java.awt.*
|
|||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
|
|
||||||
|
|
||||||
@@ -20,12 +21,14 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protected val generalOption = GeneralOption()
|
protected val generalOption = GeneralOption()
|
||||||
protected val proxyOption = ProxyOption()
|
protected val proxyOption = ProxyOption()
|
||||||
protected val terminalOption = TerminalOption()
|
protected val terminalOption = TerminalOption()
|
||||||
protected val owner: Window? get() = SwingUtilities.getWindowAncestor(this)
|
protected val jumpHostsOption = JumpHostsOption()
|
||||||
|
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addOption(generalOption)
|
addOption(generalOption)
|
||||||
addOption(proxyOption)
|
addOption(proxyOption)
|
||||||
addOption(tunnelingOption)
|
addOption(tunnelingOption)
|
||||||
|
addOption(jumpHostsOption)
|
||||||
addOption(terminalOption)
|
addOption(terminalOption)
|
||||||
|
|
||||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
@@ -46,10 +49,9 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
password = String(generalOption.passwordTextField.password)
|
password = String(generalOption.passwordTextField.password)
|
||||||
)
|
)
|
||||||
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
} else if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
val keyPair = generalOption.publicKeyTextField.getClientProperty(OhKeyPair::class) as OhKeyPair?
|
|
||||||
authentication = authentication.copy(
|
authentication = authentication.copy(
|
||||||
type = AuthenticationType.PublicKey,
|
type = AuthenticationType.PublicKey,
|
||||||
password = keyPair?.id ?: StringUtils.EMPTY
|
password = generalOption.publicKeyComboBox.selectedItem?.toString() ?: StringUtils.EMPTY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +71,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
env = terminalOption.environmentTextArea.text,
|
env = terminalOption.environmentTextArea.text,
|
||||||
startupCommand = terminalOption.startupCommandTextField.text,
|
startupCommand = terminalOption.startupCommandTextField.text,
|
||||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||||
|
jumpHosts = jumpHostsOption.jumpHosts.map { it.id }
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -107,7 +110,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
if (validateField(generalOption.publicKeyTextField)) {
|
if (validateField(generalOption.publicKeyComboBox)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +148,19 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
||||||
|
if (comboBox.isEnabled && comboBox.selectedItem == null) {
|
||||||
|
selectOptionJComponent(comboBox)
|
||||||
|
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
comboBox.requestFocusInWindow()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
val portTextField = PortSpinner()
|
val portTextField = PortSpinner()
|
||||||
val nameTextField = OutlineTextField(128)
|
val nameTextField = OutlineTextField(128)
|
||||||
@@ -154,7 +170,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
private val passwordPanel = JPanel(BorderLayout())
|
private val passwordPanel = JPanel(BorderLayout())
|
||||||
private val chooseKeyBtn = JButton(Icons.greyKey)
|
private val chooseKeyBtn = JButton(Icons.greyKey)
|
||||||
val passwordTextField = OutlinePasswordField(255)
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
val publicKeyTextField = OutlineTextField()
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
val remarkTextArea = FixedLengthTextArea(512)
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
@@ -166,7 +182,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
private fun initView() {
|
private fun initView() {
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
publicKeyTextField.isEditable = false
|
publicKeyComboBox.isEditable = false
|
||||||
chooseKeyBtn.isFocusable = false
|
chooseKeyBtn.isFocusable = false
|
||||||
|
|
||||||
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
protocolTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
@@ -187,6 +203,28 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = StringUtils.EMPTY
|
||||||
|
if (value is String) {
|
||||||
|
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
override fun getListCellRendererComponent(
|
override fun getListCellRendererComponent(
|
||||||
list: JList<*>?,
|
list: JList<*>?,
|
||||||
@@ -265,14 +303,20 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
dialog.pack()
|
dialog.pack()
|
||||||
dialog.setLocationRelativeTo(null)
|
dialog.setLocationRelativeTo(null)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
if (dialog.ok) {
|
|
||||||
val lastKeyPair = dialog.getLasOhKeyPair()
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
if (lastKeyPair != null) {
|
|
||||||
publicKeyTextField.putClientProperty(OhKeyPair::class, lastKeyPair)
|
publicKeyComboBox.removeAllItems()
|
||||||
publicKeyTextField.text = lastKeyPair.name
|
for (keyPair in KeyManager.getInstance().getOhKeyPairs()) {
|
||||||
publicKeyTextField.outline = null
|
publicKeyComboBox.addItem(keyPair.id)
|
||||||
}
|
}
|
||||||
|
publicKeyComboBox.selectedItem = selectedItem
|
||||||
|
|
||||||
|
if (!dialog.ok) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicKeyComboBox.selectedItem = dialog.getLasOhKeyPair()?.id ?: return
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshStates() {
|
private fun refreshStates() {
|
||||||
@@ -280,6 +324,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
portTextField.isEnabled = true
|
portTextField.isEnabled = true
|
||||||
usernameTextField.isEnabled = true
|
usernameTextField.isEnabled = true
|
||||||
authenticationTypeComboBox.isEnabled = true
|
authenticationTypeComboBox.isEnabled = true
|
||||||
|
publicKeyComboBox.isEnabled = true
|
||||||
passwordTextField.isEnabled = true
|
passwordTextField.isEnabled = true
|
||||||
chooseKeyBtn.isEnabled = true
|
chooseKeyBtn.isEnabled = true
|
||||||
|
|
||||||
@@ -289,6 +334,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
usernameTextField.isEnabled = false
|
usernameTextField.isEnabled = false
|
||||||
authenticationTypeComboBox.isEnabled = false
|
authenticationTypeComboBox.isEnabled = false
|
||||||
passwordTextField.isEnabled = false
|
passwordTextField.isEnabled = false
|
||||||
|
publicKeyComboBox.isEnabled = false
|
||||||
chooseKeyBtn.isEnabled = false
|
chooseKeyBtn.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,10 +411,16 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
passwordPanel.removeAll()
|
passwordPanel.removeAll()
|
||||||
|
|
||||||
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
|
publicKeyComboBox.removeAllItems()
|
||||||
|
for (pair in KeyManager.getInstance().getOhKeyPairs()) {
|
||||||
|
publicKeyComboBox.addItem(pair.id)
|
||||||
|
}
|
||||||
|
publicKeyComboBox.selectedItem = selectedItem
|
||||||
passwordPanel.add(
|
passwordPanel.add(
|
||||||
FormBuilder.create()
|
FormBuilder.create()
|
||||||
.layout(FormLayout("default:grow, 4dlu, left:pref", "pref"))
|
.layout(FormLayout("default:grow, 4dlu, left:pref", "pref"))
|
||||||
.add(publicKeyTextField).xy(1, 1)
|
.add(publicKeyComboBox).xy(1, 1)
|
||||||
.add(chooseKeyBtn).xy(3, 1)
|
.add(chooseKeyBtn).xy(3, 1)
|
||||||
.build(), BorderLayout.CENTER
|
.build(), BorderLayout.CENTER
|
||||||
)
|
)
|
||||||
@@ -635,6 +687,12 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
model.addColumn(I18n.getString("termora.new-host.tunneling.table.destination"))
|
model.addColumn(I18n.getString("termora.new-host.tunneling.table.destination"))
|
||||||
|
|
||||||
|
|
||||||
|
table.putClientProperty(
|
||||||
|
FlatClientProperties.STYLE, mapOf(
|
||||||
|
"showHorizontalLines" to true,
|
||||||
|
"showVerticalLines" to true,
|
||||||
|
)
|
||||||
|
)
|
||||||
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
||||||
table.border = BorderFactory.createEmptyBorder()
|
table.border = BorderFactory.createEmptyBorder()
|
||||||
table.fillsViewportHeight = true
|
table.fillsViewportHeight = true
|
||||||
@@ -843,4 +901,169 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected inner class JumpHostsOption : JPanel(BorderLayout()), Option {
|
||||||
|
val jumpHosts = mutableListOf<Host>()
|
||||||
|
var filter: (host: Host) -> Boolean = { true }
|
||||||
|
|
||||||
|
private val model = object : DefaultTableModel() {
|
||||||
|
|
||||||
|
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRowCount(): Int {
|
||||||
|
return jumpHosts.size
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getValueAt(row: Int, column: Int): Any {
|
||||||
|
val host = jumpHosts.getOrNull(row) ?: return StringUtils.EMPTY
|
||||||
|
return if (column == 0)
|
||||||
|
host.name
|
||||||
|
else "${host.host}:${host.port}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val table = JTable(model)
|
||||||
|
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
|
private val moveUpBtn = JButton(I18n.getString("termora.transport.bookmarks.up"))
|
||||||
|
private val moveDownBtn = JButton(I18n.getString("termora.transport.bookmarks.down"))
|
||||||
|
private val deleteBtn = JButton(I18n.getString("termora.new-host.tunneling.delete"))
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
val scrollPane = JScrollPane(table)
|
||||||
|
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.general.name"))
|
||||||
|
model.addColumn(I18n.getString("termora.new-host.general.host"))
|
||||||
|
|
||||||
|
table.putClientProperty(
|
||||||
|
FlatClientProperties.STYLE, mapOf(
|
||||||
|
"showHorizontalLines" to true,
|
||||||
|
"showVerticalLines" to true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
||||||
|
table.setDefaultRenderer(
|
||||||
|
Any::class.java,
|
||||||
|
DefaultTableCellRenderer().apply { horizontalAlignment = SwingConstants.CENTER })
|
||||||
|
table.fillsViewportHeight = true
|
||||||
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createEmptyBorder(4, 0, 4, 0),
|
||||||
|
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.BorderColor)
|
||||||
|
)
|
||||||
|
table.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
|
moveUpBtn.isFocusable = false
|
||||||
|
moveDownBtn.isFocusable = false
|
||||||
|
deleteBtn.isFocusable = false
|
||||||
|
moveUpBtn.isEnabled = false
|
||||||
|
moveDownBtn.isEnabled = false
|
||||||
|
deleteBtn.isEnabled = false
|
||||||
|
addBtn.isFocusable = false
|
||||||
|
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(addBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(deleteBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(moveUpBtn)
|
||||||
|
box.add(Box.createHorizontalStrut(4))
|
||||||
|
box.add(moveDownBtn)
|
||||||
|
|
||||||
|
add(JLabel("${getTitle()}:"), BorderLayout.NORTH)
|
||||||
|
add(scrollPane, BorderLayout.CENTER)
|
||||||
|
add(box, BorderLayout.SOUTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
|
val dialog = HostTreeDialog(owner) { host ->
|
||||||
|
jumpHosts.none { it.id == host.id } && filter.invoke(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
val hosts = dialog.hosts
|
||||||
|
if (hosts.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hosts.forEach {
|
||||||
|
val rowCount = model.rowCount
|
||||||
|
jumpHosts.add(it)
|
||||||
|
model.fireTableRowsInserted(rowCount, rowCount + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows.sortedDescending()
|
||||||
|
if (rows.isEmpty()) return
|
||||||
|
for (row in rows) {
|
||||||
|
jumpHosts.removeAt(row)
|
||||||
|
model.fireTableRowsDeleted(row, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
table.selectionModel.addListSelectionListener {
|
||||||
|
deleteBtn.isEnabled = table.selectedRowCount > 0
|
||||||
|
moveUpBtn.isEnabled = deleteBtn.isEnabled && !table.selectedRows.contains(0)
|
||||||
|
moveDownBtn.isEnabled = deleteBtn.isEnabled && !table.selectedRows.contains(table.rowCount - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
moveUpBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows.sorted()
|
||||||
|
if (rows.isEmpty()) return
|
||||||
|
|
||||||
|
table.clearSelection()
|
||||||
|
|
||||||
|
for (row in rows) {
|
||||||
|
val host = jumpHosts[(row)]
|
||||||
|
jumpHosts.removeAt(row)
|
||||||
|
jumpHosts.add(row - 1, host)
|
||||||
|
table.addRowSelectionInterval(row - 1, row - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
moveDownBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows.sortedDescending()
|
||||||
|
if (rows.isEmpty()) return
|
||||||
|
|
||||||
|
table.clearSelection()
|
||||||
|
|
||||||
|
for (row in rows) {
|
||||||
|
val host = jumpHosts[(row)]
|
||||||
|
jumpHosts.removeAt(row)
|
||||||
|
jumpHosts.add(row + 1, host)
|
||||||
|
table.addRowSelectionInterval(row + 1, row + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.server
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.jump-hosts")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,16 @@ import kotlinx.coroutines.swing.Swing
|
|||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
abstract class HostTerminalTab(
|
||||||
|
val windowScope: WindowScope,
|
||||||
|
val host: Host,
|
||||||
|
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
||||||
|
) : PropertyTerminalTab() {
|
||||||
|
companion object {
|
||||||
|
val Host = DataKey(app.termora.Host::class)
|
||||||
|
}
|
||||||
|
|
||||||
protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) }
|
protected val coroutineScope by lazy { CoroutineScope(Dispatchers.Swing) }
|
||||||
protected val terminal = TerminalFactory.instance.createTerminal()
|
|
||||||
protected val terminalModel get() = terminal.getTerminalModel()
|
protected val terminalModel get() = terminal.getTerminalModel()
|
||||||
protected var unread = false
|
protected var unread = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -25,6 +32,7 @@ abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
terminal.getTerminalModel().setData(Host, host)
|
||||||
terminal.getTerminalModel().addDataListener(object : DataListener {
|
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
if (key == VisualTerminal.Written) {
|
if (key == VisualTerminal.Written) {
|
||||||
@@ -51,6 +59,7 @@ abstract class HostTerminalTab(val host: Host) : PropertyTerminalTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
|
terminal.close()
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
|
import app.termora.actions.NewHostAction
|
||||||
|
import app.termora.actions.OpenHostAction
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||||
@@ -24,7 +26,7 @@ import javax.swing.tree.TreeSelectionModel
|
|||||||
|
|
||||||
|
|
||||||
class HostTree : JTree(), Disposable {
|
class HostTree : JTree(), Disposable {
|
||||||
private val hostManager get() = HostManager.instance
|
private val hostManager get() = HostManager.getInstance()
|
||||||
private val editor = OutlineTextField(64)
|
private val editor = OutlineTextField(64)
|
||||||
|
|
||||||
var contextmenu = true
|
var contextmenu = true
|
||||||
@@ -83,7 +85,7 @@ class HostTree : JTree(), Disposable {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
val state = Database.instance.properties.getString("HostTreeExpansionState")
|
val state = Database.getDatabase().properties.getString("HostTreeExpansionState")
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
TreeUtils.loadExpansionState(this@HostTree, state)
|
TreeUtils.loadExpansionState(this@HostTree, state)
|
||||||
}
|
}
|
||||||
@@ -132,8 +134,8 @@ class HostTree : JTree(), Disposable {
|
|||||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
val host = lastSelectedPathComponent
|
val host = lastSelectedPathComponent
|
||||||
if (host is Host && host.protocol != Protocol.Folder) {
|
if (host is Host && host.protocol != Protocol.Folder) {
|
||||||
ActionManager.getInstance().getAction(Actions.OPEN_HOST)
|
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||||
?.actionPerformed(OpenHostActionEvent(this, host))
|
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,6 +318,7 @@ class HostTree : JTree(), Disposable {
|
|||||||
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
||||||
|
|
||||||
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open"))
|
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open"))
|
||||||
|
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
||||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||||
@@ -328,15 +331,8 @@ class HostTree : JTree(), Disposable {
|
|||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||||
|
|
||||||
open.addActionListener {
|
open.addActionListener { openHosts(it, false) }
|
||||||
getSelectionNodes()
|
openInNewWindow.addActionListener { openHosts(it, true) }
|
||||||
.filter { it.protocol != Protocol.Folder }
|
|
||||||
.forEach {
|
|
||||||
ActionManager.getInstance()
|
|
||||||
.getAction(Actions.OPEN_HOST)
|
|
||||||
?.actionPerformed(OpenHostActionEvent(this, it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
||||||
@@ -412,7 +408,8 @@ class HostTree : JTree(), Disposable {
|
|||||||
|
|
||||||
newHost.addActionListener(object : AbstractAction() {
|
newHost.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
showAddHostDialog()
|
ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
|
||||||
|
?.actionPerformed(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -451,30 +448,19 @@ class HostTree : JTree(), Disposable {
|
|||||||
popupMenu.show(this, event.x, event.y)
|
popupMenu.show(this, event.x, event.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAddHostDialog() {
|
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||||
var lastHost = lastSelectedPathComponent
|
assertEventDispatchThread()
|
||||||
if (lastHost !is Host) {
|
val nodes = getSelectionNodes().filter { it.protocol != Protocol.Folder }
|
||||||
return
|
if (nodes.isEmpty()) return
|
||||||
|
val openHostAction = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||||
|
val source = if (openInNewWindow)
|
||||||
|
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||||
|
else evt.source
|
||||||
|
|
||||||
|
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastHost.protocol != Protocol.Folder) {
|
fun expandNode(node: Host, including: Boolean = false) {
|
||||||
val p = model.getParent(lastHost) ?: return
|
|
||||||
lastHost = p
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this))
|
|
||||||
dialog.isVisible = true
|
|
||||||
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
|
||||||
|
|
||||||
runCatchingHost(host)
|
|
||||||
|
|
||||||
expandNode(lastHost)
|
|
||||||
selectionPath = TreePath(model.getPathToRoot(host))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun expandNode(node: Host, including: Boolean = false) {
|
|
||||||
expandPath(TreePath(model.getPathToRoot(node)))
|
expandPath(TreePath(model.getPathToRoot(node)))
|
||||||
if (including) {
|
if (including) {
|
||||||
model.getChildren(node).forEach { expandNode(it, true) }
|
model.getChildren(node).forEach { expandNode(it, true) }
|
||||||
@@ -541,18 +527,23 @@ class HostTree : JTree(), Disposable {
|
|||||||
|
|
||||||
while (parents.isNotEmpty()) {
|
while (parents.isNotEmpty()) {
|
||||||
val p = parents.removeFirst()
|
val p = parents.removeFirst()
|
||||||
for (i in 0 until model.getChildCount(p)) {
|
for (i in 0 until getModel().getChildCount(p)) {
|
||||||
val child = model.getChild(p, i) as Host
|
val child = getModel().getChild(p, i) as Host
|
||||||
nodes.add(child)
|
nodes.add(child)
|
||||||
parents.add(child)
|
parents.add(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保是最新的
|
||||||
|
for (i in 0 until nodes.size) {
|
||||||
|
nodes[i] = model.getHost(nodes[i].id) ?: continue
|
||||||
|
}
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
Database.instance.properties.putString(
|
Database.getDatabase().properties.putString(
|
||||||
"HostTreeExpansionState",
|
"HostTreeExpansionState",
|
||||||
TreeUtils.saveExpansionState(this)
|
TreeUtils.saveExpansionState(this)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
@@ -10,7 +9,10 @@ import java.awt.event.WindowEvent
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.tree.TreeSelectionModel
|
import javax.swing.tree.TreeSelectionModel
|
||||||
|
|
||||||
class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
|
class HostTreeDialog(
|
||||||
|
owner: Window,
|
||||||
|
private val filter: (host: Host) -> Boolean = { true }
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
private val tree = HostTree()
|
private val tree = HostTree()
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
title = I18n.getString("termora.transport.sftp.select-host")
|
title = I18n.getString("termora.transport.sftp.select-host")
|
||||||
|
|
||||||
tree.setModel(SearchableHostTreeModel(tree.model) { host ->
|
tree.setModel(SearchableHostTreeModel(tree.model) { host ->
|
||||||
host.protocol == Protocol.Folder || host.protocol == Protocol.SSH
|
(host.protocol == Protocol.Folder || host.protocol == Protocol.SSH) && filter.invoke(host)
|
||||||
})
|
})
|
||||||
tree.contextmenu = true
|
tree.contextmenu = true
|
||||||
tree.doubleClickConnection = false
|
tree.doubleClickConnection = false
|
||||||
@@ -51,7 +53,7 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
addWindowListener(object : WindowAdapter() {
|
addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowActivated(e: WindowEvent) {
|
override fun windowActivated(e: WindowEvent) {
|
||||||
removeWindowListener(this)
|
removeWindowListener(this)
|
||||||
val state = Database.instance.properties.getString("HostTreeDialog.HostTreeExpansionState")
|
val state = Database.getDatabase().properties.getString("HostTreeDialog.HostTreeExpansionState")
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
TreeUtils.loadExpansionState(tree, state)
|
TreeUtils.loadExpansionState(tree, state)
|
||||||
}
|
}
|
||||||
@@ -71,7 +73,8 @@ class HostTreeDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowClosed(e: WindowEvent) {
|
override fun windowClosed(e: WindowEvent) {
|
||||||
Database.instance.properties.putString(
|
tree.setModel(null)
|
||||||
|
Database.getDatabase().properties.putString(
|
||||||
"HostTreeDialog.HostTreeExpansionState",
|
"HostTreeDialog.HostTreeExpansionState",
|
||||||
TreeUtils.saveExpansionState(tree)
|
TreeUtils.saveExpansionState(tree)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class HostTreeModel : TreeModel {
|
|||||||
|
|
||||||
val listeners = mutableListOf<TreeModelListener>()
|
val listeners = mutableListOf<TreeModelListener>()
|
||||||
|
|
||||||
private val hostManager get() = HostManager.instance
|
private val hostManager get() = HostManager.getInstance()
|
||||||
private val hosts = mutableMapOf<String, Host>()
|
private val hosts = mutableMapOf<String, Host>()
|
||||||
private val myRoot by lazy {
|
private val myRoot by lazy {
|
||||||
Host(
|
Host(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnAction
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter
|
import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter
|
||||||
import org.jdesktop.swingx.JXHyperlink
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
|
|||||||
@@ -40,12 +40,17 @@ object I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getString(key: String, vararg args: Any): String {
|
fun getString(key: String, vararg args: Any): String {
|
||||||
try {
|
val text = getString(key)
|
||||||
val text = substitutor.replace(bundle.getString(key))
|
|
||||||
if (args.isNotEmpty()) {
|
if (args.isNotEmpty()) {
|
||||||
return MessageFormat.format(text, *args)
|
return MessageFormat.format(text, *args)
|
||||||
}
|
}
|
||||||
return text
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getString(key: String): String {
|
||||||
|
try {
|
||||||
|
return substitutor.replace(bundle.getString(key))
|
||||||
} catch (e: MissingResourceException) {
|
} catch (e: MissingResourceException) {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
log.warn(e.message, e)
|
log.warn(e.message, e)
|
||||||
@@ -54,4 +59,5 @@ object I18n {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,17 +3,25 @@ package app.termora
|
|||||||
object Icons {
|
object Icons {
|
||||||
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
||||||
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||||
|
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
|
||||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||||
|
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||||
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||||
|
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
||||||
|
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
||||||
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
||||||
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
|
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
|
||||||
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
|
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
|
||||||
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
|
val dumpThreads by lazy { DynamicIcon("icons/dumpThreads.svg", "icons/dumpThreads_dark.svg") }
|
||||||
val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") }
|
val supertypes by lazy { DynamicIcon("icons/supertypes.svg", "icons/supertypes_dark.svg") }
|
||||||
|
val fitContent by lazy { DynamicIcon("icons/fitContent.svg", "icons/fitContent_dark.svg") }
|
||||||
val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
|
val settings by lazy { DynamicIcon("icons/settings.svg", "icons/settings_dark.svg") }
|
||||||
|
val copy by lazy { DynamicIcon("icons/copy.svg", "icons/copy_dark.svg") }
|
||||||
|
val delete by lazy { DynamicIcon("icons/delete.svg", "icons/delete_dark.svg") }
|
||||||
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
|
val pin by lazy { DynamicIcon("icons/pin.svg", "icons/pin_dark.svg") }
|
||||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||||
|
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||||
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
val errorIntroduction by lazy { DynamicIcon("icons/errorIntroduction.svg", "icons/errorIntroduction_dark.svg") }
|
||||||
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
val networkPolicy by lazy { DynamicIcon("icons/networkPolicy.svg", "icons/networkPolicy_dark.svg") }
|
||||||
@@ -47,11 +55,11 @@ object Icons {
|
|||||||
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
val google by lazy { DynamicIcon("icons/google-small.svg") }
|
||||||
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
val aliyun by lazy { DynamicIcon("icons/aliyun.svg") }
|
||||||
val yandexCloud by lazy { DynamicIcon("icons/yandexCloud.svg") }
|
val yandexCloud by lazy { DynamicIcon("icons/yandexCloud.svg") }
|
||||||
val aws by lazy { DynamicIcon("icons/aws.svg","icons/aws_dark.svg") }
|
val aws by lazy { DynamicIcon("icons/aws.svg", "icons/aws_dark.svg") }
|
||||||
val huawei by lazy { DynamicIcon("icons/huawei.svg") }
|
val huawei by lazy { DynamicIcon("icons/huawei.svg") }
|
||||||
val baidu by lazy { DynamicIcon("icons/baiduyun.svg") }
|
val baidu by lazy { DynamicIcon("icons/baiduyun.svg") }
|
||||||
val tianyi by lazy { DynamicIcon("icons/tianyiyun.svg") }
|
val tianyi by lazy { DynamicIcon("icons/tianyiyun.svg") }
|
||||||
val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg","icons/digitalocean_dark.svg") }
|
val digitalocean by lazy { DynamicIcon("icons/digitalocean.svg", "icons/digitalocean_dark.svg") }
|
||||||
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
|
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
|
||||||
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
|
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
|
||||||
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
|
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
|
||||||
@@ -73,8 +81,23 @@ object Icons {
|
|||||||
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") }
|
||||||
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
|
val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") }
|
||||||
val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") }
|
val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") }
|
||||||
|
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") }
|
||||||
|
val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||||
|
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
|
||||||
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
val fileTransfer by lazy { DynamicIcon("icons/fileTransfer.svg", "icons/fileTransfer_dark.svg") }
|
||||||
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
val help by lazy { DynamicIcon("icons/help.svg", "icons/help_dark.svg") }
|
||||||
|
val applyNotConflictsLeft by lazy {
|
||||||
|
DynamicIcon(
|
||||||
|
"icons/applyNotConflictsLeft.svg",
|
||||||
|
"icons/applyNotConflictsLeft_dark.svg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val applyNotConflictsRight by lazy {
|
||||||
|
DynamicIcon(
|
||||||
|
"icons/applyNotConflictsRight.svg",
|
||||||
|
"icons/applyNotConflictsRight_dark.svg"
|
||||||
|
)
|
||||||
|
}
|
||||||
val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") }
|
val expand by lazy { DynamicIcon("icons/expand.svg", "icons/expand_dark.svg") }
|
||||||
val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") }
|
val collapse by lazy { DynamicIcon("icons/collapse.svg", "icons/collapse_dark.svg") }
|
||||||
val expandAll by lazy { DynamicIcon("icons/expandAll.svg", "icons/expandAll_dark.svg") }
|
val expandAll by lazy { DynamicIcon("icons/expandAll.svg", "icons/expandAll_dark.svg") }
|
||||||
|
|||||||
@@ -9,7 +9,58 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
class LightLaf : FlatLightLaf(), ColorTheme {
|
interface LafTag
|
||||||
|
interface LightLafTag : LafTag
|
||||||
|
interface DarkLafTag : LafTag
|
||||||
|
|
||||||
|
class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
|
||||||
|
putAll(
|
||||||
|
mapOf(
|
||||||
|
"@baseTheme" to "dark",
|
||||||
|
"@background" to "#282935",
|
||||||
|
"@windowText" to "#eaeaea",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}), ColorTheme, DarkLafTag {
|
||||||
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
return when (color) {
|
||||||
|
TerminalColor.Basic.BACKGROUND -> 0x282935
|
||||||
|
TerminalColor.Basic.FOREGROUND -> 0xeaeaea
|
||||||
|
TerminalColor.Basic.SELECTION_BACKGROUND -> 0x56596b
|
||||||
|
TerminalColor.Basic.SELECTION_FOREGROUND -> 0xfeffff
|
||||||
|
TerminalColor.Basic.HYPERLINK -> 0x255ab4
|
||||||
|
|
||||||
|
TerminalColor.Cursor.BACKGROUND -> 0xc7c7c7
|
||||||
|
|
||||||
|
TerminalColor.Find.BACKGROUND -> 0xffff00
|
||||||
|
TerminalColor.Find.FOREGROUND -> 0x282935
|
||||||
|
|
||||||
|
TerminalColor.Normal.BLACK -> 0
|
||||||
|
TerminalColor.Normal.RED -> 0xef766d
|
||||||
|
TerminalColor.Normal.GREEN -> 0x88f397
|
||||||
|
TerminalColor.Normal.YELLOW -> 0xf4f8a7
|
||||||
|
TerminalColor.Normal.BLUE -> 0xc4a9f4
|
||||||
|
TerminalColor.Normal.MAGENTA -> 0xf297cd
|
||||||
|
TerminalColor.Normal.CYAN -> 0xaceafb
|
||||||
|
TerminalColor.Normal.WHITE -> 0xc7c7c7
|
||||||
|
|
||||||
|
TerminalColor.Bright.BLACK -> 0x676767
|
||||||
|
TerminalColor.Bright.RED -> 0xef766d
|
||||||
|
TerminalColor.Bright.GREEN -> 0x88f397
|
||||||
|
TerminalColor.Bright.YELLOW -> 0xf4f8a7
|
||||||
|
TerminalColor.Bright.BLUE -> 0xc4a9f4
|
||||||
|
TerminalColor.Bright.MAGENTA -> 0xf297cd
|
||||||
|
TerminalColor.Bright.CYAN -> 0xaceafb
|
||||||
|
TerminalColor.Bright.WHITE -> 0xfeffff
|
||||||
|
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LightLaf : FlatLightLaf(), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0
|
TerminalColor.Normal.BLACK -> 0
|
||||||
@@ -36,7 +87,7 @@ class LightLaf : FlatLightLaf(), ColorTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DarkLaf : FlatDarkLaf(), ColorTheme {
|
class DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0
|
TerminalColor.Normal.BLACK -> 0
|
||||||
@@ -65,7 +116,7 @@ class DarkLaf : FlatDarkLaf(), ColorTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme {
|
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
|
|
||||||
@@ -113,7 +164,7 @@ class TermiusLightLaf : FlatPropertiesLaf("Termius Light", Properties().apply {
|
|||||||
"@windowText" to "#32364a",
|
"@windowText" to "#32364a",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
|
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
|
|
||||||
@@ -156,14 +207,14 @@ class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply {
|
|||||||
"@windowText" to "#21b568",
|
"@windowText" to "#21b568",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
|
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
TerminalColor.Cursor.BACKGROUND -> 0x21b568
|
TerminalColor.Cursor.BACKGROUND -> 0x21b568
|
||||||
|
|
||||||
TerminalColor.Basic.SELECTION_FOREGROUND ->0
|
TerminalColor.Basic.SELECTION_FOREGROUND -> 0
|
||||||
|
|
||||||
TerminalColor.Basic.FOREGROUND -> 0x21b568
|
TerminalColor.Basic.FOREGROUND -> 0x21b568
|
||||||
|
|
||||||
@@ -198,7 +249,7 @@ class NovelLaf : FlatPropertiesLaf("Novel", Properties().apply {
|
|||||||
"@windowText" to "#3b2322",
|
"@windowText" to "#3b2322",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -237,7 +288,7 @@ class AtomOneDarkLaf : FlatPropertiesLaf("Atom One Dark", Properties().apply {
|
|||||||
"@windowText" to "#abb2bf",
|
"@windowText" to "#abb2bf",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -275,7 +326,7 @@ class AtomOneLightLaf : FlatPropertiesLaf("Atom One Light", Properties().apply {
|
|||||||
"@windowText" to "#383a42",
|
"@windowText" to "#383a42",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -313,7 +364,7 @@ class EverforestDarkLaf : FlatPropertiesLaf("Everforest Dark", Properties().appl
|
|||||||
"@windowText" to "#d3c6aa",
|
"@windowText" to "#d3c6aa",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x42494e
|
TerminalColor.Normal.BLACK -> 0x42494e
|
||||||
@@ -350,7 +401,7 @@ class EverforestLightLaf : FlatPropertiesLaf("Everforest Light", Properties().ap
|
|||||||
"@windowText" to "#5c6a72",
|
"@windowText" to "#5c6a72",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x42494e
|
TerminalColor.Normal.BLACK -> 0x42494e
|
||||||
@@ -387,7 +438,7 @@ class NightOwlLaf : FlatPropertiesLaf("Night Owl", Properties().apply {
|
|||||||
"@windowText" to "#d6deeb",
|
"@windowText" to "#d6deeb",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x072945
|
TerminalColor.Normal.BLACK -> 0x072945
|
||||||
@@ -424,7 +475,7 @@ class LightOwlLaf : FlatPropertiesLaf("Light Owl", Properties().apply {
|
|||||||
"@windowText" to "#403f53",
|
"@windowText" to "#403f53",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x403f53
|
TerminalColor.Normal.BLACK -> 0x403f53
|
||||||
@@ -461,7 +512,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
|
|||||||
"@windowText" to "#edecee",
|
"@windowText" to "#edecee",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x1c1b22
|
TerminalColor.Normal.BLACK -> 0x1c1b22
|
||||||
@@ -498,7 +549,7 @@ class Cobalt2Laf : FlatPropertiesLaf("Cobalt2", Properties().apply {
|
|||||||
"@windowText" to "#ffffff",
|
"@windowText" to "#ffffff",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -535,7 +586,7 @@ class OctocatDarkLaf : FlatPropertiesLaf("Octocat Dark", Properties().apply {
|
|||||||
"@windowText" to "#8b949e",
|
"@windowText" to "#8b949e",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -572,7 +623,7 @@ class OctocatLightLaf : FlatPropertiesLaf("Octocat Light", Properties().apply {
|
|||||||
"@windowText" to "#3e3e3e",
|
"@windowText" to "#3e3e3e",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -609,7 +660,7 @@ class AyuDarkLaf : FlatPropertiesLaf("Ayu Dark", Properties().apply {
|
|||||||
"@windowText" to "#e6e1cf",
|
"@windowText" to "#e6e1cf",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -646,7 +697,7 @@ class AyuLightLaf : FlatPropertiesLaf("Ayu Light", Properties().apply {
|
|||||||
"@windowText" to "#5c6773",
|
"@windowText" to "#5c6773",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -683,7 +734,7 @@ class HomebrewLaf : FlatPropertiesLaf("Homebrew", Properties().apply {
|
|||||||
"@windowText" to "#00ff00",
|
"@windowText" to "#00ff00",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
||||||
@@ -722,7 +773,7 @@ class ProLaf : FlatPropertiesLaf("Pro", Properties().apply {
|
|||||||
"@windowText" to "#f2f2f2",
|
"@windowText" to "#f2f2f2",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
TerminalColor.Normal.BLACK -> 0x2e2e2e
|
||||||
@@ -761,7 +812,7 @@ class NordLightLaf : FlatPropertiesLaf("Nord Light", Properties().apply {
|
|||||||
"@windowText" to "#414858",
|
"@windowText" to "#414858",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x2c3344
|
TerminalColor.Normal.BLACK -> 0x2c3344
|
||||||
@@ -800,7 +851,7 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
|
|||||||
"@windowText" to "#d8dee9",
|
"@windowText" to "#d8dee9",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x3b4252
|
TerminalColor.Normal.BLACK -> 0x3b4252
|
||||||
@@ -840,7 +891,7 @@ class GitHubLightLaf : FlatPropertiesLaf("GitHub Light", Properties().apply {
|
|||||||
"@windowText" to "#3e3e3e",
|
"@windowText" to "#3e3e3e",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, LightLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x3e3e3e
|
TerminalColor.Normal.BLACK -> 0x3e3e3e
|
||||||
@@ -879,7 +930,7 @@ class GitHubDarkLaf : FlatPropertiesLaf("GitHub Dark", Properties().apply {
|
|||||||
"@windowText" to "#8b949e",
|
"@windowText" to "#8b949e",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x000000
|
TerminalColor.Normal.BLACK -> 0x000000
|
||||||
@@ -919,7 +970,7 @@ class ChalkLaf : FlatPropertiesLaf("Chalk", Properties().apply {
|
|||||||
"@windowText" to "#d2d8d9",
|
"@windowText" to "#d2d8d9",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}), ColorTheme {
|
}), ColorTheme, DarkLafTag {
|
||||||
override fun getColor(color: TerminalColor): Int {
|
override fun getColor(color: TerminalColor): Int {
|
||||||
return when (color) {
|
return when (color) {
|
||||||
TerminalColor.Normal.BLACK -> 0x7d8b8f
|
TerminalColor.Normal.BLACK -> 0x7d8b8f
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import app.termora.terminal.PtyConnector
|
|||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class LocalTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
class LocalTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val winSize = terminalPanel.winSize()
|
val winSize = terminalPanel.winSize()
|
||||||
val ptyConnector = PtyConnectorFactory.instance.createPtyConnector(
|
val ptyConnector = PtyConnectorFactory.getInstance(windowScope).createPtyConnector(
|
||||||
winSize.rows, winSize.cols,
|
winSize.rows, winSize.cols,
|
||||||
host.options.envs(),
|
host.options.envs(),
|
||||||
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||||
|
|||||||
@@ -1,6 +1,44 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import com.pty4j.util.PtyUtil
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
|
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
setupNativeLibraries()
|
||||||
|
}
|
||||||
|
|
||||||
ApplicationRunner().run()
|
ApplicationRunner().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun setupNativeLibraries() {
|
||||||
|
if (!SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val appPath = Application.getAppPath()
|
||||||
|
if (StringUtils.isBlank(appPath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val contents = File(appPath).parentFile?.parentFile ?: return
|
||||||
|
val dylib = FileUtils.getFile(contents, "app", "dylib")
|
||||||
|
if (!dylib.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val jna = FileUtils.getFile(dylib, "jna")
|
||||||
|
if (jna.exists()) {
|
||||||
|
System.setProperty("jna.boot.library.path", jna.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pty4j = FileUtils.getFile(dylib, "pty4j")
|
||||||
|
if (pty4j.exists()) {
|
||||||
|
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
@@ -7,10 +8,15 @@ import org.jdesktop.swingx.action.ActionManager
|
|||||||
/**
|
/**
|
||||||
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
|
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
|
||||||
*/
|
*/
|
||||||
class MultiplePtyConnector(private val myConnector: PtyConnector) : PtyConnectorDelegate(myConnector) {
|
class MultiplePtyConnector(
|
||||||
|
private val myConnector: PtyConnector
|
||||||
|
) : PtyConnectorDelegate(myConnector) {
|
||||||
|
|
||||||
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
|
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
|
||||||
private val ptyConnectors get() = PtyConnectorFactory.instance.getPtyConnectors()
|
private val ptyConnectors
|
||||||
|
get() = ApplicationScope.forApplicationScope()
|
||||||
|
.windowScopes().map { PtyConnectorFactory.getInstance(it).getPtyConnectors() }
|
||||||
|
.flatten()
|
||||||
|
|
||||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
|
import app.termora.actions.ActionManager
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import app.termora.terminal.TerminalColor
|
import app.termora.terminal.TerminalColor
|
||||||
import app.termora.terminal.TextStyle
|
import app.termora.terminal.TextStyle
|
||||||
import app.termora.terminal.panel.TerminalDisplay
|
import app.termora.terminal.panel.TerminalDisplay
|
||||||
import app.termora.terminal.panel.TerminalPaintListener
|
import app.termora.terminal.panel.TerminalPaintListener
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,190 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.event.*
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class MyTabbedPane : FlatTabbedPane() {
|
class MyTabbedPane : FlatTabbedPane() {
|
||||||
|
|
||||||
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private val dragMouseAdaptor = DragMouseAdaptor()
|
||||||
|
private val terminalTabbedManager
|
||||||
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateUI() {
|
||||||
|
styleMap = mapOf(
|
||||||
|
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||||
|
"hoverColor" to UIManager.getColor("TabbedPane.background"),
|
||||||
|
)
|
||||||
|
super.updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addMouseListener(dragMouseAdaptor)
|
||||||
|
addMouseMotionListener(dragMouseAdaptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processMouseEvent(e: MouseEvent) {
|
||||||
|
// Shift + Click ===> close tab
|
||||||
|
if (e.id == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(e) && isShiftPressedOnly(e.modifiersEx)) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
tabCloseCallback?.accept(this, index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (e.id == MouseEvent.MOUSE_PRESSED && isShiftPressedOnly(e.modifiersEx)) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.processMouseEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isShiftPressedOnly(modifiersEx: Int): Boolean {
|
||||||
|
return (modifiersEx and InputEvent.ALT_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.CTRL_DOWN_MASK) == 0
|
||||||
|
&& (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0
|
||||||
|
}
|
||||||
|
|
||||||
override fun setSelectedIndex(index: Int) {
|
override fun setSelectedIndex(index: Int) {
|
||||||
val oldIndex = selectedIndex
|
val oldIndex = selectedIndex
|
||||||
super.setSelectedIndex(index)
|
super.setSelectedIndex(index)
|
||||||
firePropertyChange("selectedIndex", oldIndex,index)
|
firePropertyChange("selectedIndex", oldIndex, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
|
||||||
|
private var mousePressedPoint = Point()
|
||||||
|
private var tabIndex = 0 - 1
|
||||||
|
private var cancelled = false
|
||||||
|
private var window: Window? = null
|
||||||
|
private var terminalTab: TerminalTab? = null
|
||||||
|
private var isDragging = false
|
||||||
|
private var lastVisitTabIndex = -1
|
||||||
|
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
val index = indexAtLocation(e.x, e.y)
|
||||||
|
if (index < 0 || !isTabClosable(index)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tabIndex = index
|
||||||
|
mousePressedPoint = e.point
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
|
// 如果正在拖拽中,那么修改 Window 的位置
|
||||||
|
if (isDragging) {
|
||||||
|
window?.location = e.locationOnScreen
|
||||||
|
lastVisitTabIndex = indexAtLocation(e.x, e.y)
|
||||||
|
} else if (tabIndex >= 0) { // 这里之所以判断是确保在 mousePressed 时已经确定了 Tab
|
||||||
|
// 有的时候会太灵敏,这里容错一下
|
||||||
|
val diff = 5
|
||||||
|
if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) {
|
||||||
|
startDrag(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDrag(e: MouseEvent) {
|
||||||
|
if (isDragging) return
|
||||||
|
val terminalTabbedManager = terminalTabbedManager ?: return
|
||||||
|
val window = JDialog(owner).also { this.window = it }
|
||||||
|
window.isUndecorated = true
|
||||||
|
val image = createTabImage(tabIndex)
|
||||||
|
window.size = Dimension(image.width, image.height)
|
||||||
|
window.add(JLabel(ImageIcon(image)))
|
||||||
|
window.location = e.locationOnScreen
|
||||||
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.removeKeyEventDispatcher(this@DragMouseAdaptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.addKeyEventDispatcher(this@DragMouseAdaptor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暂时关闭 Tab
|
||||||
|
terminalTabbedManager.closeTerminalTab(terminalTabbedManager.getTerminalTabs()[tabIndex].also {
|
||||||
|
terminalTab = it
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
window.isVisible = true
|
||||||
|
|
||||||
|
isDragging = true
|
||||||
|
cancelled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopDrag() {
|
||||||
|
if (!isDragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val tab = this.terminalTab
|
||||||
|
val terminalTabbedManager = terminalTabbedManager
|
||||||
|
|
||||||
|
if (tab != null && terminalTabbedManager != null) {
|
||||||
|
// 如果是手动取消
|
||||||
|
if (cancelled) {
|
||||||
|
terminalTabbedManager.addTerminalTab(tabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex > 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(lastVisitTabIndex, tab)
|
||||||
|
} else if (lastVisitTabIndex == 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(1, tab)
|
||||||
|
} else {
|
||||||
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset
|
||||||
|
window?.dispose()
|
||||||
|
isDragging = false
|
||||||
|
tabIndex = -1
|
||||||
|
cancelled = false
|
||||||
|
lastVisitTabIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
stopDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTabImage(index: Int): BufferedImage {
|
||||||
|
val tabBounds = getBoundsAt(index)
|
||||||
|
val image = BufferedImage(tabBounds.width, tabBounds.height, BufferedImage.TYPE_INT_ARGB)
|
||||||
|
val g2 = image.createGraphics()
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
|
||||||
|
g2.translate(-tabBounds.x, -tabBounds.y)
|
||||||
|
paint(g2)
|
||||||
|
g2.dispose()
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||||
|
if (e.keyCode == KeyEvent.VK_ESCAPE) {
|
||||||
|
cancelled = true
|
||||||
|
stopDrag()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import java.awt.event.ActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class OpenHostActionEvent(source: Any, val host: Host) : ActionEvent(source, ACTION_PERFORMED, String())
|
class OpenHostActionEvent(source: Any, val host: Host, event: EventObject) :
|
||||||
|
AnActionEvent(source, String(), event)
|
||||||
@@ -6,6 +6,7 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.JXLabel
|
import org.jdesktop.swingx.JXLabel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
@@ -122,7 +123,7 @@ object OptionPane {
|
|||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop()
|
if (Desktop.isDesktopSupported() && Desktop.getDesktop()
|
||||||
.isSupported(Desktop.Action.BROWSE_FILE_DIR)
|
.isSupported(Desktop.Action.BROWSE_FILE_DIR)
|
||||||
) {
|
) {
|
||||||
if (JOptionPane.YES_OPTION == showConfirmDialog(
|
if (yMessage.isEmpty() || JOptionPane.YES_OPTION == showConfirmDialog(
|
||||||
parentComponent,
|
parentComponent,
|
||||||
yMessage,
|
yMessage,
|
||||||
optionType = JOptionPane.YES_NO_OPTION
|
optionType = JOptionPane.YES_NO_OPTION
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
import app.termora.macro.MacroPtyConnector
|
import app.termora.macro.MacroPtyConnector
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
import app.termora.terminal.PtyProcessConnector
|
import app.termora.terminal.PtyProcessConnector
|
||||||
import com.pty4j.PtyProcessBuilder
|
import com.pty4j.PtyProcessBuilder
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class PtyConnectorFactory {
|
class PtyConnectorFactory : Disposable {
|
||||||
private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>())
|
private val ptyConnectors = Collections.synchronizedList(mutableListOf<PtyConnector>())
|
||||||
private val database get() = Database.instance
|
private val database get() = Database.getDatabase()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { PtyConnectorFactory() }
|
private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
|
||||||
|
fun getInstance(scope: Scope): PtyConnectorFactory {
|
||||||
|
return scope.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPtyConnector(
|
fun createPtyConnector(
|
||||||
@@ -29,8 +33,28 @@ class PtyConnectorFactory {
|
|||||||
envs["TERM"] = "xterm-256color"
|
envs["TERM"] = "xterm-256color"
|
||||||
envs.putAll(env)
|
envs.putAll(env)
|
||||||
|
|
||||||
|
if (SystemUtils.IS_OS_UNIX) {
|
||||||
|
if (!envs.containsKey("LANG")) {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
if (StringUtils.isNoneBlank(locale.language, locale.country)) {
|
||||||
|
envs["LANG"] = "${locale.language}_${locale.country}.${Charset.defaultCharset().name()}"
|
||||||
|
} else {
|
||||||
|
envs["LANG"] = "en_US.UTF-8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val command = database.terminal.localShell
|
val command = database.terminal.localShell
|
||||||
val ptyProcess = PtyProcessBuilder(arrayOf(command))
|
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)
|
.setEnvironment(envs)
|
||||||
.setInitialRows(rows)
|
.setInitialRows(rows)
|
||||||
.setInitialColumns(cols)
|
.setInitialColumns(cols)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.termora
|
|||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@@ -11,9 +12,14 @@ class PtyConnectorReader(
|
|||||||
private val terminal: Terminal,
|
private val terminal: Terminal,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(PtyConnectorReader::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun start() {
|
suspend fun start() {
|
||||||
var i: Int
|
var i: Int
|
||||||
val buffer = CharArray(1024 * 8)
|
val buffer = CharArray(1024 * 8)
|
||||||
|
|
||||||
while ((ptyConnector.read(buffer).also { i = it }) != -1) {
|
while ((ptyConnector.read(buffer).also { i = it }) != -1) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
delay(10.milliseconds)
|
delay(10.milliseconds)
|
||||||
@@ -22,6 +28,10 @@ class PtyConnectorReader(
|
|||||||
val text = String(buffer, 0, i)
|
val text = String(buffer, 0, i)
|
||||||
SwingUtilities.invokeLater { terminal.write(text) }
|
SwingUtilities.invokeLater { terminal.write(text) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("PtyConnectorReader stopped")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.*
|
||||||
import app.termora.terminal.PtyConnector
|
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
|
||||||
import app.termora.terminal.TerminalKeyEvent
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
@@ -12,7 +9,12 @@ import java.awt.event.KeyEvent
|
|||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
|
abstract class PtyHostTerminalTab(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
host: Host,
|
||||||
|
terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
||||||
|
) : HostTerminalTab(windowScope, host, terminal) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
@@ -21,8 +23,13 @@ abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
|
|||||||
private var readerJob: Job? = null
|
private var readerJob: Job? = null
|
||||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||||
|
|
||||||
protected val terminalPanel = TerminalPanelFactory.instance.createTerminalPanel(terminal, ptyConnectorDelegate)
|
protected val terminalPanel =
|
||||||
protected val ptyConnectorFactory get() = PtyConnectorFactory.instance
|
TerminalPanelFactory.getInstance(windowScope).createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||||
|
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance(windowScope)
|
||||||
|
|
||||||
|
init {
|
||||||
|
terminal.getTerminalModel().setData(DataKey.PtyConnector, ptyConnectorDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
@@ -60,6 +67,10 @@ abstract class PtyHostTerminalTab(host: Host) : HostTerminalTab(host) {
|
|||||||
if (log.isErrorEnabled) {
|
if (log.isErrorEnabled) {
|
||||||
log.error(e.message, e)
|
log.error(e.message, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 失败关闭
|
||||||
|
stop()
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
terminal.write("\r\n${ControlCharacters.ESC}[31m")
|
terminal.write("\r\n${ControlCharacters.ESC}[31m")
|
||||||
terminal.write(ExceptionUtils.getRootCauseMessage(e))
|
terminal.write(ExceptionUtils.getRootCauseMessage(e))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.transport.TransportDataProviders
|
||||||
import app.termora.transport.TransportPanel
|
import app.termora.transport.TransportPanel
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
@@ -20,7 +21,7 @@ class SFTPTerminalTab : Disposable, TerminalTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Icon {
|
override fun getIcon(): Icon {
|
||||||
return Icons.fileTransfer
|
return Icons.folder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
||||||
@@ -34,11 +35,14 @@ class SFTPTerminalTab : Disposable, TerminalTab {
|
|||||||
return transportPanel
|
return transportPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun canClone(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun canClose(): Boolean {
|
override fun canClose(): Boolean {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
|
||||||
if (transportPanel.transportManager.getTransports().isEmpty()) {
|
if (transportManager.getTransports().isEmpty()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.TabReconnectAction
|
||||||
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
||||||
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import app.termora.keymap.KeyShortcut
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
@@ -24,9 +28,10 @@ import org.apache.sshd.common.util.net.SshdSocketAddress
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.JComponent
|
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 {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
@@ -76,6 +81,9 @@ class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val client = SshClients.openClient(host).also { sshClient = it }
|
val client = SshClients.openClient(host).also { sshClient = it }
|
||||||
|
// keyboard interactive
|
||||||
|
client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel))
|
||||||
|
|
||||||
val sessionListener = MySessionListener()
|
val sessionListener = MySessionListener()
|
||||||
val channelListener = MyChannelListener()
|
val channelListener = MyChannelListener()
|
||||||
|
|
||||||
@@ -104,10 +112,18 @@ class SSHTerminalTab(host: Host) : PtyHostTerminalTab(host) {
|
|||||||
|
|
||||||
|
|
||||||
channel.addChannelListener(object : ChannelListener {
|
channel.addChannelListener(object : ChannelListener {
|
||||||
|
private val reconnectShortcut
|
||||||
|
get() = KeymapManager.getInstance().getActiveKeymap()
|
||||||
|
.getShortcut(TabReconnectAction.RECONNECT_TAB).firstOrNull()
|
||||||
|
|
||||||
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
override fun channelClosed(channel: Channel, reason: Throwable?) {
|
||||||
coroutineScope.launch(Dispatchers.Swing) {
|
coroutineScope.launch(Dispatchers.Swing) {
|
||||||
terminal.write("\r\n${ControlCharacters.ESC}[31m")
|
terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
|
||||||
terminal.write("Channel has been disconnected.\r\n")
|
terminal.write("Channel has been disconnected.")
|
||||||
|
if (reconnectShortcut is KeyShortcut) {
|
||||||
|
terminal.write(" Type $reconnectShortcut to reconnect.")
|
||||||
|
}
|
||||||
|
terminal.write("\r\n")
|
||||||
terminal.write("${ControlCharacters.ESC}[0m")
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
terminalModel.setData(DataKey.ShowCursor, false)
|
terminalModel.setData(DataKey.ShowCursor, false)
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/main/kotlin/app/termora/Scope.kt
Normal file
173
src/main/kotlin/app/termora/Scope.kt
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Window
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
open class Scope(
|
||||||
|
private val beans: MutableMap<KClass<*>, Any> = ConcurrentHashMap(),
|
||||||
|
private val properties: MutableMap<String, Any> = ConcurrentHashMap()
|
||||||
|
) : Disposable {
|
||||||
|
|
||||||
|
|
||||||
|
fun <T : Any> get(clazz: KClass<T>): T {
|
||||||
|
return beans[clazz] as T
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun <T : Any> getOrCreate(clazz: KClass<T>, create: () -> T): T {
|
||||||
|
|
||||||
|
if (beans.containsKey(clazz)) {
|
||||||
|
return get(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(clazz) {
|
||||||
|
if (beans.containsKey(clazz)) {
|
||||||
|
return get(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
val instance = create.invoke()
|
||||||
|
beans[clazz] = instance
|
||||||
|
|
||||||
|
if (instance is Disposable) {
|
||||||
|
Disposer.register(this, instance)
|
||||||
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun putBoolean(name: String, value: Boolean) {
|
||||||
|
properties[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBoolean(name: String, defaultValue: Boolean): Boolean {
|
||||||
|
return properties[name]?.toString()?.toBoolean() ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAny(name: String, value: Any) {
|
||||||
|
properties[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAny(name: String, defaultValue: Any): Any {
|
||||||
|
return properties[name]?.toString() ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAnyOrNull(name: String): Any? {
|
||||||
|
return properties[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
beans.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationScope private constructor() : Scope() {
|
||||||
|
|
||||||
|
private val scopes = mutableMapOf<Any, WindowScope>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(ApplicationScope::class.java)
|
||||||
|
private val instance by lazy { ApplicationScope() }
|
||||||
|
|
||||||
|
fun forApplicationScope(): ApplicationScope {
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
fun forWindowScope(frame: TermoraFrame): WindowScope {
|
||||||
|
return forApplicationScope().forWindowScope(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun forWindowScope(container: Component): WindowScope {
|
||||||
|
val frame = getFrameForComponent(container)
|
||||||
|
?: throw IllegalStateException("Unexpected owner in $container")
|
||||||
|
return forWindowScope(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun windowScopes(): List<WindowScope> {
|
||||||
|
return forApplicationScope().windowScopes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFrameForComponent(component: Component): TermoraFrame? {
|
||||||
|
if (component is TermoraFrame) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
|
var owner = SwingUtilities.getWindowAncestor(component) as Component?
|
||||||
|
if (owner is TermoraFrame) {
|
||||||
|
return owner
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owner == null) {
|
||||||
|
owner = component
|
||||||
|
}
|
||||||
|
|
||||||
|
while (owner != null) {
|
||||||
|
|
||||||
|
if (owner is JPopupMenu) {
|
||||||
|
owner = owner.invoker
|
||||||
|
if (owner is TermoraFrame) {
|
||||||
|
return owner
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
owner = owner.parent
|
||||||
|
if (owner is TermoraFrame) {
|
||||||
|
return owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun forWindowScope(frame: TermoraFrame): WindowScope {
|
||||||
|
val windowScope = scopes.getOrPut(frame) { WindowScope(frame) }
|
||||||
|
Disposer.register(windowScope, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
scopes.remove(frame)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return windowScope
|
||||||
|
}
|
||||||
|
|
||||||
|
fun windowScopes(): List<WindowScope> {
|
||||||
|
return scopes.values.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("ApplicationScope disposed")
|
||||||
|
}
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WindowScope(
|
||||||
|
val window: Window,
|
||||||
|
) : Scope() {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(WindowScope::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("WindowScope disposed")
|
||||||
|
}
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,10 +48,10 @@ class SearchableHostTreeModel(
|
|||||||
val children = model.getChildren(parent)
|
val children = model.getChildren(parent)
|
||||||
if (children.isEmpty()) return emptyList()
|
if (children.isEmpty()) return emptyList()
|
||||||
return children.filter { e ->
|
return children.filter { e ->
|
||||||
filter.invoke(e) && e.name.contains(text, true) || TreeUtils.children(model, e, true)
|
filter.invoke(e)
|
||||||
.filterIsInstance<Host>().any {
|
&& e.name.contains(text, true)
|
||||||
it.name.contains(text, true)
|
|| e.host.contains(text, true)
|
||||||
}
|
|| TreeUtils.children(model, e, true).filterIsInstance<Host>().any { it.name.contains(text, true) || it.host.contains(text, true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@@ -13,7 +12,7 @@ import javax.swing.UIManager
|
|||||||
|
|
||||||
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
private val optionsPane = SettingsOptionsPane()
|
private val optionsPane = SettingsOptionsPane()
|
||||||
private val properties get() = Database.instance.properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
|
||||||
init {
|
init {
|
||||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.AES.encodeBase64String
|
import app.termora.AES.encodeBase64String
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.db.Database
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.highlight.KeywordHighlight
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
|
import app.termora.keymap.Keymap
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.keymap.KeymapPanel
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import app.termora.macro.Macro
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.native.FileChooser
|
import app.termora.native.FileChooser
|
||||||
import app.termora.sync.SyncConfig
|
import app.termora.sync.SyncConfig
|
||||||
@@ -15,11 +22,9 @@ import app.termora.terminal.CursorStyle
|
|||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import com.formdev.flatlaf.FlatLaf
|
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.FlatButton
|
import com.formdev.flatlaf.extras.components.*
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
import com.formdev.flatlaf.extras.components.FlatLabel
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -28,19 +33,17 @@ import com.sun.jna.LastErrorException
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.*
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.jdesktop.swingx.JXEditorPane
|
import org.jdesktop.swingx.JXEditorPane
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -48,12 +51,19 @@ import java.nio.charset.StandardCharsets
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.DocumentEvent
|
import javax.swing.event.DocumentEvent
|
||||||
|
import javax.swing.event.PopupMenuEvent
|
||||||
|
import javax.swing.event.PopupMenuListener
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
class SettingsOptionsPane : OptionsPane() {
|
class SettingsOptionsPane : OptionsPane() {
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
||||||
private val database get() = Database.instance
|
private val database get() = Database.getDatabase()
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val keymapManager get() = KeymapManager.getInstance()
|
||||||
|
private val macroManager get() = MacroManager.getInstance()
|
||||||
|
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
|
private val keyManager get() = KeyManager.getInstance()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
|
||||||
@@ -96,6 +106,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
init {
|
init {
|
||||||
addOption(AppearanceOption())
|
addOption(AppearanceOption())
|
||||||
addOption(TerminalOption())
|
addOption(TerminalOption())
|
||||||
|
addOption(KeyShortcutsOption())
|
||||||
addOption(CloudSyncOption())
|
addOption(CloudSyncOption())
|
||||||
addOption(DoormanOption())
|
addOption(DoormanOption())
|
||||||
addOption(AboutOption())
|
addOption(AboutOption())
|
||||||
@@ -103,10 +114,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private inner class AppearanceOption : JPanel(BorderLayout()), Option {
|
private inner class AppearanceOption : JPanel(BorderLayout()), Option {
|
||||||
val themeManager = ThemeManager.instance
|
val themeManager = ThemeManager.getInstance()
|
||||||
val themeComboBox = FlatComboBox<String>()
|
val themeComboBox = FlatComboBox<String>()
|
||||||
val languageComboBox = FlatComboBox<String>()
|
val languageComboBox = FlatComboBox<String>()
|
||||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||||
|
val preferredThemeBtn = JButton(Icons.settings)
|
||||||
private val appearance get() = database.appearance
|
private val appearance get() = database.appearance
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -117,6 +129,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|
||||||
followSystemCheckBox.isSelected = appearance.followSystem
|
followSystemCheckBox.isSelected = appearance.followSystem
|
||||||
|
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||||
|
|
||||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||||
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
|
||||||
@@ -156,19 +169,17 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
followSystemCheckBox.addActionListener {
|
followSystemCheckBox.addActionListener {
|
||||||
appearance.followSystem = followSystemCheckBox.isSelected
|
appearance.followSystem = followSystemCheckBox.isSelected
|
||||||
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
themeComboBox.isEnabled = !followSystemCheckBox.isSelected
|
||||||
|
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
|
||||||
|
appearance.theme = themeComboBox.selectedItem as String
|
||||||
|
|
||||||
if (followSystemCheckBox.isSelected) {
|
if (followSystemCheckBox.isSelected) {
|
||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
if (OsThemeDetector.getDetector().isDark) {
|
if (OsThemeDetector.getDetector().isDark) {
|
||||||
if (!FlatLaf.isLafDark()) {
|
themeManager.change(appearance.darkTheme)
|
||||||
themeManager.change("Dark")
|
themeComboBox.selectedItem = appearance.darkTheme
|
||||||
themeComboBox.selectedItem = "Dark"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (FlatLaf.isLafDark()) {
|
themeManager.change(appearance.lightTheme)
|
||||||
themeManager.change("Light")
|
themeComboBox.selectedItem = appearance.lightTheme
|
||||||
themeComboBox.selectedItem = "Light"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +198,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -201,23 +214,78 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showPreferredThemeContextmenu() {
|
||||||
|
val popupMenu = FlatPopupMenu()
|
||||||
|
val dark = JMenu("For Dark OS")
|
||||||
|
val light = JMenu("For Light OS")
|
||||||
|
val darkTheme = appearance.darkTheme
|
||||||
|
val lightTheme = appearance.lightTheme
|
||||||
|
|
||||||
|
for (e in themeManager.themes) {
|
||||||
|
val clazz = Class.forName(e.value)
|
||||||
|
val item = JCheckBoxMenuItem(e.key)
|
||||||
|
item.isSelected = e.key == lightTheme || e.key == darkTheme
|
||||||
|
if (clazz.interfaces.contains(DarkLafTag::class.java)) {
|
||||||
|
dark.add(item).addActionListener {
|
||||||
|
if (e.key != darkTheme) {
|
||||||
|
appearance.darkTheme = e.key
|
||||||
|
if (OsThemeDetector.getDetector().isDark) {
|
||||||
|
themeComboBox.selectedItem = e.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (clazz.interfaces.contains(LightLafTag::class.java)) {
|
||||||
|
light.add(item).addActionListener {
|
||||||
|
if (e.key != lightTheme) {
|
||||||
|
appearance.lightTheme = e.key
|
||||||
|
if (!OsThemeDetector.getDetector().isDark) {
|
||||||
|
themeComboBox.selectedItem = e.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popupMenu.add(dark)
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
popupMenu.add(light)
|
||||||
|
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
popupMenu.show(preferredThemeBtn, 0, preferredThemeBtn.height + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getFormPanel(): JPanel {
|
private fun getFormPanel(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin"
|
"pref, $formMargin, pref, $formMargin"
|
||||||
)
|
)
|
||||||
|
val box = FlatToolBar()
|
||||||
|
box.add(followSystemCheckBox)
|
||||||
|
box.add(Box.createHorizontalStrut(2))
|
||||||
|
box.add(preferredThemeBtn)
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
return FormBuilder.create().layout(layout)
|
return FormBuilder.create().layout(layout)
|
||||||
.add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows)
|
||||||
.add(themeComboBox).xy(3, rows)
|
.add(themeComboBox).xy(3, rows)
|
||||||
.add(followSystemCheckBox).xy(5, rows).apply { rows += step }
|
.add(box).xy(5, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows)
|
||||||
.add(languageComboBox).xy(3, rows)
|
.add(languageComboBox).xy(3, rows)
|
||||||
.add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) {
|
.add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) {
|
||||||
override fun actionPerformed(evt: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
|
Application.browse(URI.create("https://github.com/TermoraDev/termora/tree/main/src/main/resources/i18n"))
|
||||||
}
|
}
|
||||||
})).xy(5, rows).apply { rows += step }
|
})).xy(5, rows).apply { rows += step }
|
||||||
@@ -234,7 +302,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val shellComboBox = FlatComboBox<String>()
|
private val shellComboBox = FlatComboBox<String>()
|
||||||
private val maxRowsTextField = IntSpinner(0, 0)
|
private val maxRowsTextField = IntSpinner(0, 0)
|
||||||
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
||||||
private val terminalSetting get() = Database.instance.terminal
|
private val terminalSetting get() = Database.getDatabase().terminal
|
||||||
private val selectCopyComboBox = YesOrNoComboBox()
|
private val selectCopyComboBox = YesOrNoComboBox()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -270,7 +338,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
val style = cursorStyleComboBox.selectedItem as CursorStyle
|
val style = cursorStyleComboBox.selectedItem as CursorStyle
|
||||||
terminalSetting.cursor = style
|
terminalSetting.cursor = style
|
||||||
TerminalFactory.instance.getTerminals().forEach { e ->
|
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
|
||||||
e.getTerminalModel().setData(DataKey.CursorStyle, style)
|
e.getTerminalModel().setData(DataKey.CursorStyle, style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +348,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
debugComboBox.addItemListener { e ->
|
debugComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.debug = debugComboBox.selectedItem as Boolean
|
terminalSetting.debug = debugComboBox.selectedItem as Boolean
|
||||||
TerminalFactory.instance.getTerminals().forEach {
|
TerminalFactory.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
|
||||||
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
|
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,7 +364,10 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fireFontChanged() {
|
private fun fireFontChanged() {
|
||||||
TerminalPanelFactory.instance.fireResize()
|
ApplicationScope.windowScopes().forEach {
|
||||||
|
TerminalPanelFactory.getInstance(it)
|
||||||
|
.fireResize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -318,6 +389,28 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
if (value is String) {
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
"<html><font face='$value'>$value</font></html>",
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontComboBox.maximumSize = fontComboBox.preferredSize
|
||||||
|
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Block)
|
cursorStyleComboBox.addItem(CursorStyle.Block)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
||||||
@@ -330,8 +423,33 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
fontComboBox.addItem("JetBrains Mono")
|
val fonts = linkedSetOf(
|
||||||
fontComboBox.addItem("Source Code Pro")
|
"JetBrains Mono",
|
||||||
|
"Source Code Pro",
|
||||||
|
"Monospaced",
|
||||||
|
"Andale Mono",
|
||||||
|
"Ayuthaya",
|
||||||
|
"Courier New",
|
||||||
|
"Droid Sans Mono",
|
||||||
|
"Fira Code",
|
||||||
|
"PCMyungjo",
|
||||||
|
"Menlo",
|
||||||
|
"Monaco",
|
||||||
|
"Osaka",
|
||||||
|
"PT Mono",
|
||||||
|
"SimSong",
|
||||||
|
)
|
||||||
|
|
||||||
|
for (font in FontUtils.getAllFonts()) {
|
||||||
|
if (fonts.contains(font.family)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fonts.remove(font.family)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (font in fonts) {
|
||||||
|
fontComboBox.addItem(font)
|
||||||
|
}
|
||||||
|
|
||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
@@ -390,6 +508,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val domainTextField = OutlineTextField(255)
|
val domainTextField = OutlineTextField(255)
|
||||||
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload)
|
||||||
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
|
||||||
|
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import)
|
||||||
val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
|
val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download)
|
||||||
val lastSyncTimeLabel = JLabel()
|
val lastSyncTimeLabel = JLabel()
|
||||||
val sync get() = database.sync
|
val sync get() = database.sync
|
||||||
@@ -397,6 +516,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
||||||
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
||||||
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
||||||
|
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
|
||||||
val visitGistBtn = JButton(Icons.externalLink)
|
val visitGistBtn = JButton(Icons.externalLink)
|
||||||
val getTokenBtn = JButton(Icons.externalLink)
|
val getTokenBtn = JButton(Icons.externalLink)
|
||||||
|
|
||||||
@@ -489,13 +609,18 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
getTokenBtn.addActionListener {
|
getTokenBtn.addActionListener {
|
||||||
when (typeComboBox.selectedItem) {
|
when (typeComboBox.selectedItem) {
|
||||||
SyncType.GitLab -> Application.browse(URI.create("https://gitlab.com/-/user_settings/personal_access_tokens"))
|
SyncType.GitLab -> {
|
||||||
|
val uri = URI.create(domainTextField.text)
|
||||||
|
Application.browse(URI.create("${uri.scheme}://${uri.host}/-/user_settings/personal_access_tokens?name=Termora%20Sync%20Config&scopes=api"))
|
||||||
|
}
|
||||||
|
|
||||||
SyncType.GitHub -> Application.browse(URI.create("https://github.com/settings/tokens"))
|
SyncType.GitHub -> Application.browse(URI.create("https://github.com/settings/tokens"))
|
||||||
SyncType.Gitee -> Application.browse(URI.create("https://gitee.com/profile/personal_access_tokens"))
|
SyncType.Gitee -> Application.browse(URI.create("https://gitee.com/profile/personal_access_tokens"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exportConfigButton.addActionListener { export() }
|
exportConfigButton.addActionListener { export() }
|
||||||
|
importConfigButton.addActionListener { import() }
|
||||||
|
|
||||||
keysCheckBox.addActionListener { refreshButtons() }
|
keysCheckBox.addActionListener { refreshButtons() }
|
||||||
hostsCheckBox.addActionListener { refreshButtons() }
|
hostsCheckBox.addActionListener { refreshButtons() }
|
||||||
@@ -512,6 +637,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|| keywordHighlightsCheckBox.isSelected
|
|| keywordHighlightsCheckBox.isSelected
|
||||||
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
exportConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
|
importConfigButton.isEnabled = downloadConfigButton.isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun export() {
|
private fun export() {
|
||||||
@@ -527,6 +653,109 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun import() {
|
||||||
|
val fileChooser = FileChooser()
|
||||||
|
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
|
fileChooser.osxAllowedFileTypes = listOf("json")
|
||||||
|
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
||||||
|
fileChooser.showOpenDialog(owner).thenAccept { files ->
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
SwingUtilities.invokeLater { importFromFile(files.first()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importFromFile(file: File) {
|
||||||
|
if (!file.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val ranges = getSyncConfig().ranges
|
||||||
|
if (ranges.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最大 100MB
|
||||||
|
if (file.length() >= 1024 * 1024 * 100) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.settings.sync.import.file-too-large"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = file.readText()
|
||||||
|
val jsonResult = ohMyJson.runCatching { decodeFromString<JsonObject>(text) }
|
||||||
|
if (jsonResult.isFailure) {
|
||||||
|
val e = jsonResult.exceptionOrNull() ?: return
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = jsonResult.getOrNull() ?: return
|
||||||
|
if (ranges.contains(SyncRange.Hosts)) {
|
||||||
|
val hosts = json["hosts"]
|
||||||
|
if (hosts is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<Host>>(hosts.jsonArray) }.onSuccess {
|
||||||
|
for (host in it) {
|
||||||
|
hostManager.addHost(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.KeyPairs)) {
|
||||||
|
val keyPairs = json["keyPairs"]
|
||||||
|
if (keyPairs is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<OhKeyPair>>(keyPairs.jsonArray) }.onSuccess {
|
||||||
|
for (keyPair in it) {
|
||||||
|
keyManager.addOhKeyPair(keyPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
|
val keywordHighlights = json["keywordHighlights"]
|
||||||
|
if (keywordHighlights is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<KeywordHighlight>>(keywordHighlights.jsonArray) }
|
||||||
|
.onSuccess {
|
||||||
|
for (keyPair in it) {
|
||||||
|
keywordHighlightManager.addKeywordHighlight(keyPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Macros)) {
|
||||||
|
val macros = json["macros"]
|
||||||
|
if (macros is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<Macro>>(macros.jsonArray) }.onSuccess {
|
||||||
|
for (macro in it) {
|
||||||
|
macroManager.addMacro(macro)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymaps = json["keymaps"]
|
||||||
|
if (keymaps is JsonArray) {
|
||||||
|
for (keymap in keymaps.jsonArray.mapNotNull { Keymap.fromJSON(it.jsonObject) }) {
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.settings.sync.import.successful"),
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun exportText(file: File) {
|
private fun exportText(file: File) {
|
||||||
val syncConfig = getSyncConfig()
|
val syncConfig = getSyncConfig()
|
||||||
val text = ohMyJson.encodeToString(buildJsonObject {
|
val text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
@@ -537,21 +766,29 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
put("os", SystemUtils.OS_NAME)
|
put("os", SystemUtils.OS_NAME)
|
||||||
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
|
put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now)))
|
||||||
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
||||||
put("hosts", ohMyJson.encodeToJsonElement(HostManager.instance.hosts()))
|
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.instance.getOhKeyPairs()))
|
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
put(
|
put(
|
||||||
"keywordHighlights",
|
"keywordHighlights",
|
||||||
ohMyJson.encodeToJsonElement(KeywordHighlightManager.instance.getKeywordHighlights())
|
ohMyJson.encodeToJsonElement(keywordHighlightManager.getKeywordHighlights())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.Macros)) {
|
if (syncConfig.ranges.contains(SyncRange.Macros)) {
|
||||||
put(
|
put(
|
||||||
"macros",
|
"macros",
|
||||||
ohMyJson.encodeToJsonElement(MacroManager.instance.getMacros())
|
ohMyJson.encodeToJsonElement(macroManager.getMacros())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (syncConfig.ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymaps = keymapManager.getKeymaps().filter { !it.isReadonly }
|
||||||
|
.map { it.toJSONObject() }
|
||||||
|
put(
|
||||||
|
"keymaps",
|
||||||
|
ohMyJson.encodeToJsonElement(keymaps)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
put("settings", buildJsonObject {
|
put("settings", buildJsonObject {
|
||||||
@@ -584,6 +821,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (macrosCheckBox.isSelected) {
|
if (macrosCheckBox.isSelected) {
|
||||||
range.add(SyncRange.Macros)
|
range.add(SyncRange.Macros)
|
||||||
}
|
}
|
||||||
|
if (keymapCheckBox.isSelected) {
|
||||||
|
range.add(SyncRange.Keymap)
|
||||||
|
}
|
||||||
return SyncConfig(
|
return SyncConfig(
|
||||||
type = typeComboBox.selectedItem as SyncType,
|
type = typeComboBox.selectedItem as SyncType,
|
||||||
token = String(tokenTextField.password),
|
token = String(tokenTextField.password),
|
||||||
@@ -593,6 +833,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
private suspend fun pushOrPull(push: Boolean) {
|
private suspend fun pushOrPull(push: Boolean) {
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
@@ -648,12 +889,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
exportConfigButton.isEnabled = false
|
exportConfigButton.isEnabled = false
|
||||||
|
importConfigButton.isEnabled = false
|
||||||
downloadConfigButton.isEnabled = false
|
downloadConfigButton.isEnabled = false
|
||||||
uploadConfigButton.isEnabled = false
|
uploadConfigButton.isEnabled = false
|
||||||
typeComboBox.isEnabled = false
|
typeComboBox.isEnabled = false
|
||||||
gistTextField.isEnabled = false
|
gistTextField.isEnabled = false
|
||||||
tokenTextField.isEnabled = false
|
tokenTextField.isEnabled = false
|
||||||
keysCheckBox.isEnabled = false
|
keysCheckBox.isEnabled = false
|
||||||
|
macrosCheckBox.isEnabled = false
|
||||||
|
keymapCheckBox.isEnabled = false
|
||||||
keywordHighlightsCheckBox.isEnabled = false
|
keywordHighlightsCheckBox.isEnabled = false
|
||||||
hostsCheckBox.isEnabled = false
|
hostsCheckBox.isEnabled = false
|
||||||
domainTextField.isEnabled = false
|
domainTextField.isEnabled = false
|
||||||
@@ -669,7 +913,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
// sync
|
// sync
|
||||||
val syncResult = kotlin.runCatching {
|
val syncResult = kotlin.runCatching {
|
||||||
val syncer = SyncerProvider.instance.getSyncer(syncConfig.type)
|
val syncer = SyncerProvider.getInstance().getSyncer(syncConfig.type)
|
||||||
if (push) {
|
if (push) {
|
||||||
syncer.push(syncConfig)
|
syncer.push(syncConfig)
|
||||||
} else {
|
} else {
|
||||||
@@ -681,10 +925,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
downloadConfigButton.isEnabled = true
|
downloadConfigButton.isEnabled = true
|
||||||
exportConfigButton.isEnabled = true
|
exportConfigButton.isEnabled = true
|
||||||
|
importConfigButton.isEnabled = true
|
||||||
uploadConfigButton.isEnabled = true
|
uploadConfigButton.isEnabled = true
|
||||||
keysCheckBox.isEnabled = true
|
keysCheckBox.isEnabled = true
|
||||||
hostsCheckBox.isEnabled = true
|
hostsCheckBox.isEnabled = true
|
||||||
typeComboBox.isEnabled = true
|
typeComboBox.isEnabled = true
|
||||||
|
macrosCheckBox.isEnabled = true
|
||||||
|
keymapCheckBox.isEnabled = true
|
||||||
gistTextField.isEnabled = true
|
gistTextField.isEnabled = true
|
||||||
tokenTextField.isEnabled = true
|
tokenTextField.isEnabled = true
|
||||||
domainTextField.isEnabled = true
|
domainTextField.isEnabled = true
|
||||||
@@ -745,11 +992,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
keysCheckBox.isFocusable = false
|
keysCheckBox.isFocusable = false
|
||||||
keywordHighlightsCheckBox.isFocusable = false
|
keywordHighlightsCheckBox.isFocusable = false
|
||||||
macrosCheckBox.isFocusable = false
|
macrosCheckBox.isFocusable = false
|
||||||
|
keymapCheckBox.isFocusable = false
|
||||||
|
|
||||||
hostsCheckBox.isSelected = sync.rangeHosts
|
hostsCheckBox.isSelected = sync.rangeHosts
|
||||||
keysCheckBox.isSelected = sync.rangeKeyPairs
|
keysCheckBox.isSelected = sync.rangeKeyPairs
|
||||||
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
||||||
macrosCheckBox.isSelected = sync.rangeMacros
|
macrosCheckBox.isSelected = sync.rangeMacros
|
||||||
|
keymapCheckBox.isSelected = sync.rangeKeymap
|
||||||
|
|
||||||
typeComboBox.selectedItem = sync.type
|
typeComboBox.selectedItem = sync.type
|
||||||
gistTextField.text = sync.gist
|
gistTextField.text = sync.gist
|
||||||
@@ -812,11 +1061,12 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(keysCheckBox).xy(3, 1)
|
.add(keysCheckBox).xy(3, 1)
|
||||||
.add(keywordHighlightsCheckBox).xy(5, 1)
|
.add(keywordHighlightsCheckBox).xy(5, 1)
|
||||||
.add(macrosCheckBox).xy(1, 3)
|
.add(macrosCheckBox).xy(1, 3)
|
||||||
|
.add(keymapCheckBox).xy(3, 3)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val builder = FormBuilder.create().layout(layout).debug(false);
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
val box = Box.createHorizontalBox()
|
val box = Box.createHorizontalBox()
|
||||||
box.add(typeComboBox)
|
box.add(typeComboBox)
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
@@ -835,10 +1085,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
// Sync buttons
|
// Sync buttons
|
||||||
.add(
|
.add(
|
||||||
FormBuilder.create()
|
FormBuilder.create()
|
||||||
.layout(FormLayout("left:pref, $formMargin, left:pref, $formMargin, left:pref", "pref"))
|
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref"))
|
||||||
.add(uploadConfigButton).xy(1, 1)
|
.add(uploadConfigButton).xy(1, 1)
|
||||||
.add(downloadConfigButton).xy(3, 1)
|
.add(downloadConfigButton).xy(3, 1)
|
||||||
.add(exportConfigButton).xy(5, 1)
|
.add(exportConfigButton).xy(5, 1)
|
||||||
|
.add(importConfigButton).xy(7, 1)
|
||||||
.build()
|
.build()
|
||||||
).xy(3, rows, "center, fill").apply { rows += step }
|
).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
@@ -872,6 +1123,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
|
|
||||||
|
val branch = if (Application.isUnknownVersion()) "main" else Application.getVersion()
|
||||||
|
|
||||||
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
||||||
.layout(layout).debug(true)
|
.layout(layout).debug(true)
|
||||||
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
|
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
|
||||||
@@ -881,7 +1134,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add("${I18n.getString("termora.settings.about.source")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.about.source")}:").xy(1, rows)
|
||||||
.add(
|
.add(
|
||||||
createHyperlink(
|
createHyperlink(
|
||||||
"https://github.com/TermoraDev/termora/tree/${Application.getVersion()}",
|
"https://github.com/TermoraDev/termora/tree/${branch}",
|
||||||
"https://github.com/TermoraDev/termora",
|
"https://github.com/TermoraDev/termora",
|
||||||
)
|
)
|
||||||
).xy(3, rows).apply { rows += step }
|
).xy(3, rows).apply { rows += step }
|
||||||
@@ -890,7 +1143,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add("${I18n.getString("termora.settings.about.third-party")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.about.third-party")}:").xy(1, rows)
|
||||||
.add(
|
.add(
|
||||||
createHyperlink(
|
createHyperlink(
|
||||||
"https://github.com/TermoraDev/termora/blob/${Application.getVersion()}/THIRDPARTY",
|
"https://github.com/TermoraDev/termora/blob/${branch}/THIRDPARTY",
|
||||||
"Open-source software"
|
"Open-source software"
|
||||||
)
|
)
|
||||||
).xy(3, rows).apply { rows += step }
|
).xy(3, rows).apply { rows += step }
|
||||||
@@ -901,10 +1154,10 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun createHyperlink(url: String, text: String = url): Hyperlink {
|
private fun createHyperlink(url: String, text: String = url): Hyperlink {
|
||||||
return Hyperlink(object : AnAction(text) {
|
return Hyperlink(object : AnAction(text) {
|
||||||
override fun actionPerformed(evt: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
Application.browse(URI.create(url))
|
Application.browse(URI.create(url))
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {}
|
private fun initEvents() {}
|
||||||
@@ -930,9 +1183,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val twoPasswordTextField = OutlinePasswordField(255)
|
private val twoPasswordTextField = OutlinePasswordField(255)
|
||||||
private val tip = FlatLabel()
|
private val tip = FlatLabel()
|
||||||
private val safeBtn = FlatButton()
|
private val safeBtn = FlatButton()
|
||||||
private val doorman get() = Doorman.instance
|
private val doorman get() = Doorman.getInstance()
|
||||||
private val hostManager get() = HostManager.instance
|
|
||||||
private val keyManager get() = KeyManager.instance
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -1155,5 +1406,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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,14 @@ import app.termora.terminal.TerminalSize
|
|||||||
import org.apache.sshd.client.ClientBuilder
|
import org.apache.sshd.client.ClientBuilder
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
|
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
||||||
|
import org.apache.sshd.client.kex.DHGClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
||||||
import org.apache.sshd.common.global.KeepAliveHandler
|
import org.apache.sshd.common.global.KeepAliveHandler
|
||||||
|
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||||
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
import org.apache.sshd.core.CoreModuleProperties
|
import org.apache.sshd.core.CoreModuleProperties
|
||||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
||||||
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
||||||
@@ -16,14 +20,15 @@ import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
|||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
import org.eclipse.jgit.transport.CredentialsProvider
|
||||||
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.ProxyData
|
import org.eclipse.jgit.transport.sshd.ProxyData
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
|
||||||
|
|
||||||
object SshClients {
|
object SshClients {
|
||||||
private val timeout = Duration.ofSeconds(30)
|
private val timeout = Duration.ofSeconds(30)
|
||||||
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开一个 Shell
|
* 打开一个 Shell
|
||||||
@@ -57,6 +62,54 @@ object SshClients {
|
|||||||
* 打开一个会话
|
* 打开一个会话
|
||||||
*/
|
*/
|
||||||
fun openSession(host: Host, client: SshClient): ClientSession {
|
fun openSession(host: Host, client: SshClient): ClientSession {
|
||||||
|
|
||||||
|
|
||||||
|
// 如果没有跳板机直接连接
|
||||||
|
if (host.options.jumpHosts.isEmpty()) {
|
||||||
|
return doOpenSession(host, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
val jumpHosts = mutableListOf<Host>()
|
||||||
|
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||||
|
for (jumpHostId in host.options.jumpHosts) {
|
||||||
|
val e = hosts[jumpHostId]
|
||||||
|
if (e == null) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Failed to find jump host: $jumpHostId")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
jumpHosts.add(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后一跳是目标机器
|
||||||
|
jumpHosts.add(host)
|
||||||
|
|
||||||
|
val sessions = mutableListOf<ClientSession>()
|
||||||
|
for (i in 0 until jumpHosts.size) {
|
||||||
|
val currentHost = jumpHosts[i]
|
||||||
|
sessions.add(doOpenSession(currentHost, client))
|
||||||
|
|
||||||
|
// 如果有下一跳
|
||||||
|
if (i < jumpHosts.size - 1) {
|
||||||
|
val nextHost = jumpHosts[i + 1]
|
||||||
|
// 通过 currentHost 的 Session 将远程端口映射到本地
|
||||||
|
val address = sessions.last().startLocalPortForwarding(
|
||||||
|
SshdSocketAddress.LOCALHOST_ADDRESS,
|
||||||
|
SshdSocketAddress(nextHost.host, nextHost.port),
|
||||||
|
)
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
|
||||||
|
}
|
||||||
|
// 映射完毕之后修改Host和端口
|
||||||
|
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doOpenSession(host: Host, client: SshClient): ClientSession {
|
||||||
val session = client.connect(host.username, host.host, host.port)
|
val session = client.connect(host.username, host.host, host.port)
|
||||||
.verify(timeout).session
|
.verify(timeout).session
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
@@ -64,12 +117,16 @@ object SshClients {
|
|||||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||||
}
|
}
|
||||||
if (!session.auth().verify(timeout).await(timeout)) {
|
|
||||||
|
val verifyTimeout = Duration.ofSeconds(timeout.seconds * 5)
|
||||||
|
if (!session.auth().verify(verifyTimeout).await(verifyTimeout)) {
|
||||||
throw SshException("Authentication failed")
|
throw SshException("Authentication failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开一个客户端
|
* 打开一个客户端
|
||||||
*/
|
*/
|
||||||
@@ -78,7 +135,19 @@ object SshClients {
|
|||||||
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
|
||||||
.factory { JGitSshClient() }
|
.factory { JGitSshClient() }
|
||||||
|
|
||||||
if (host.tunnelings.isEmpty()) {
|
val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/123
|
||||||
|
keyExchangeFactories.addAll(
|
||||||
|
listOf(
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhg1),
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhg14),
|
||||||
|
DHGClient.newFactory(BuiltinDHFactories.dhgex),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
builder.keyExchangeFactories(keyExchangeFactories)
|
||||||
|
|
||||||
|
if (host.tunnelings.isEmpty() && host.options.jumpHosts.isEmpty()) {
|
||||||
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
builder.forwardingFilter(RejectAllForwardingFilter.INSTANCE)
|
||||||
} else {
|
} else {
|
||||||
builder.forwardingFilter(AcceptAllForwardingFilter.INSTANCE)
|
builder.forwardingFilter(AcceptAllForwardingFilter.INSTANCE)
|
||||||
@@ -89,6 +158,8 @@ object SshClients {
|
|||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||||
|
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||||
|
|
||||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||||
|
|
||||||
if (host.proxy.type != ProxyType.No) {
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
|
import app.termora.tlog.TerminalLoggerDataListener
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
|
|
||||||
class TerminalFactory {
|
class TerminalFactory private constructor() : Disposable {
|
||||||
private val terminals = mutableListOf<Terminal>()
|
private val terminals = mutableListOf<Terminal>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { TerminalFactory() }
|
fun getInstance(scope: WindowScope): TerminalFactory {
|
||||||
|
return scope.getOrCreate(TerminalFactory::class) { TerminalFactory() }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun createTerminal(): Terminal {
|
fun createTerminal(): Terminal {
|
||||||
val terminal = MyVisualTerminal()
|
val terminal = MyVisualTerminal()
|
||||||
|
|
||||||
|
// terminal logger listener
|
||||||
|
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
||||||
|
|
||||||
terminals.add(terminal)
|
terminals.add(terminal)
|
||||||
return terminal
|
return terminal
|
||||||
}
|
}
|
||||||
@@ -23,7 +30,7 @@ class TerminalFactory {
|
|||||||
return terminals
|
return terminals
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyVisualTerminal : VisualTerminal() {
|
open class MyVisualTerminal : VisualTerminal() {
|
||||||
private val terminalModel by lazy { MyTerminalModel(this) }
|
private val terminalModel by lazy { MyTerminalModel(this) }
|
||||||
|
|
||||||
override fun getTerminalModel(): TerminalModel {
|
override fun getTerminalModel(): TerminalModel {
|
||||||
@@ -31,13 +38,13 @@ class TerminalFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
|
open class MyTerminalModel(terminal: Terminal) : TerminalModelImpl(terminal) {
|
||||||
private val colorPalette by lazy { MyColorPalette(terminal) }
|
private val colorPalette by lazy { MyColorPalette(terminal) }
|
||||||
private val config get() = Database.instance.terminal
|
private val config get() = Database.getDatabase().terminal
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setData(DataKey.CursorStyle, config.cursor)
|
this.setData(DataKey.CursorStyle, config.cursor)
|
||||||
setData(TerminalPanel.Debug, config.debug)
|
this.setData(TerminalPanel.Debug, config.debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getColorPalette(): ColorPalette {
|
override fun getColorPalette(): ColorPalette {
|
||||||
@@ -90,17 +97,19 @@ class TerminalFactory {
|
|||||||
TerminalColor.Basic.SELECTION_FOREGROUND
|
TerminalColor.Basic.SELECTION_FOREGROUND
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> DefaultColorTheme.instance.getColor(color)
|
else -> DefaultColorTheme.getInstance().getColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyColorPalette(terminal: Terminal) : ColorPaletteImpl(terminal) {
|
class MyColorPalette(terminal: Terminal) : ColorPaletteImpl(terminal) {
|
||||||
private val colorTheme by lazy { FlatLafColorTheme() }
|
private val colorTheme by lazy { FlatLafColorTheme() }
|
||||||
override fun getTheme(): ColorTheme {
|
override fun getTheme(): ColorTheme {
|
||||||
return colorTheme
|
return colorTheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,16 @@ class TerminalPanelFactory {
|
|||||||
private val terminalPanels = mutableListOf<TerminalPanel>()
|
private val terminalPanels = mutableListOf<TerminalPanel>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy { TerminalPanelFactory() }
|
fun getInstance(scope: Scope): TerminalPanelFactory {
|
||||||
|
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
val terminalPanel = TerminalPanel(terminal, ptyConnector)
|
val terminalPanel = TerminalPanel(terminal, ptyConnector)
|
||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.instance)
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.instance)
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
terminalPanels.add(terminalPanel)
|
terminalPanels.add(terminalPanel)
|
||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,10 @@ interface TerminalTab : Disposable {
|
|||||||
*/
|
*/
|
||||||
fun canClose(): Boolean = true
|
fun canClose(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以克隆
|
||||||
|
*/
|
||||||
|
fun canClone(): Boolean = true
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
|
import app.termora.actions.DataProviderSupport
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@@ -11,7 +15,9 @@ class TerminalTabDialog(
|
|||||||
owner: Window,
|
owner: Window,
|
||||||
size: Dimension,
|
size: Dimension,
|
||||||
private val terminalTab: TerminalTab
|
private val terminalTab: TerminalTab
|
||||||
) : DialogWrapper(null), Disposable {
|
) : DialogWrapper(null), Disposable, DataProvider {
|
||||||
|
|
||||||
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
title = terminalTab.getTitle()
|
title = terminalTab.getTitle()
|
||||||
@@ -19,6 +25,7 @@ class TerminalTabDialog(
|
|||||||
isAlwaysOnTop = false
|
isAlwaysOnTop = false
|
||||||
iconImages = owner.iconImages
|
iconImages = owner.iconImages
|
||||||
escapeDispose = false
|
escapeDispose = false
|
||||||
|
processGlobalKeymap = true
|
||||||
|
|
||||||
super.setSize(size)
|
super.setSize(size)
|
||||||
|
|
||||||
@@ -34,6 +41,15 @@ class TerminalTabDialog(
|
|||||||
})
|
})
|
||||||
|
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
|
||||||
|
if (owner is DataProvider) {
|
||||||
|
owner.getData(DataProviders.WindowScope)?.let {
|
||||||
|
dataProviderSupport.addData(DataProviders.WindowScope, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataProviderSupport.addData(DataProviders.TerminalTab, terminalTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createSouthPanel(): JComponent? {
|
override fun createSouthPanel(): JComponent? {
|
||||||
@@ -52,4 +68,8 @@ class TerminalTabDialog(
|
|||||||
super<DialogWrapper>.dispose()
|
super<DialogWrapper>.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
return dataProviderSupport.getData(dataKey)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,41 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
|
import app.termora.actions.*
|
||||||
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhere
|
|
||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.transport.TransportPanel
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
import java.awt.*
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import java.awt.event.AWTEventListener
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.awt.Component
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeEvent
|
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import javax.swing.*
|
import java.util.*
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPanel
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class TerminalTabbed(
|
class TerminalTabbed(
|
||||||
private val toolbar: JToolBar,
|
private val windowScope: WindowScope,
|
||||||
|
private val termoraToolBar: TermoraToolBar,
|
||||||
private val tabbedPane: FlatTabbedPane,
|
private val tabbedPane: FlatTabbedPane,
|
||||||
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager {
|
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
|
||||||
private val tabs = mutableListOf<TerminalTab>()
|
private val tabs = mutableListOf<TerminalTab>()
|
||||||
|
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||||
|
private val toolbar = termoraToolBar.getJToolBar()
|
||||||
|
private val actionManager = ActionManager.getInstance()
|
||||||
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
private val titleProperty = UUID.randomUUID().toSimpleString()
|
||||||
private val iconListener = PropertyChangeListener { e ->
|
private val iconListener = PropertyChangeListener { e ->
|
||||||
val source = e.source
|
val source = e.source
|
||||||
if (e.propertyName == "icon" && source is TerminalTab) {
|
if (e.propertyName == "icon" && source is TerminalTab) {
|
||||||
@@ -50,40 +57,14 @@ class TerminalTabbed(
|
|||||||
tabbedPane.isTabsClosable = true
|
tabbedPane.isTabsClosable = true
|
||||||
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
||||||
|
|
||||||
tabbedPane.styleMap = mapOf(
|
|
||||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground")
|
|
||||||
)
|
|
||||||
|
|
||||||
val actionManager = ActionManager.getInstance()
|
|
||||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
|
||||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
|
||||||
updateBtn.isVisible = updateBtn.isEnabled
|
|
||||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
|
||||||
|
|
||||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
|
||||||
override fun actionPerformed(e: ActionEvent?) {
|
|
||||||
actionManager.getAction(Actions.FIND_EVERYWHERE)?.actionPerformed(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnabled(): Boolean {
|
|
||||||
return actionManager.getAction(Actions.FIND_EVERYWHERE)?.isEnabled ?: false
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
toolbar.add(Box.createHorizontalStrut(UIManager.getInt("TabbedPane.tabHeight")))
|
|
||||||
toolbar.add(Box.createHorizontalGlue())
|
|
||||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MACRO)))
|
|
||||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEYWORD_HIGHLIGHT_EVERYWHERE)))
|
|
||||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.KEY_MANAGER)))
|
|
||||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.MULTIPLE)))
|
|
||||||
toolbar.add(updateBtn)
|
|
||||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.FIND_EVERYWHERE)))
|
|
||||||
toolbar.add(actionContainerFactory.createButton(actionManager.getAction(Actions.SETTING)))
|
|
||||||
|
|
||||||
|
|
||||||
tabbedPane.trailingComponent = toolbar
|
tabbedPane.trailingComponent = toolbar
|
||||||
|
|
||||||
add(tabbedPane, BorderLayout.CENTER)
|
add(tabbedPane, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
|
||||||
|
|
||||||
|
dataProviderSupport.addData(DataProviders.TerminalTabbed, this)
|
||||||
|
dataProviderSupport.addData(DataProviders.TerminalTabbedManager, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -92,8 +73,7 @@ class TerminalTabbed(
|
|||||||
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
||||||
|
|
||||||
// 选中变动
|
// 选中变动
|
||||||
tabbedPane.addPropertyChangeListener("selectedIndex", object : PropertyChangeListener {
|
tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
|
||||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
|
||||||
val oldIndex = evt.oldValue as Int
|
val oldIndex = evt.oldValue as Int
|
||||||
val newIndex = evt.newValue as Int
|
val newIndex = evt.newValue as Int
|
||||||
if (oldIndex >= 0 && tabs.size > newIndex) {
|
if (oldIndex >= 0 && tabs.size > newIndex) {
|
||||||
@@ -103,7 +83,6 @@ class TerminalTabbed(
|
|||||||
tabs[newIndex].onGrabFocus()
|
tabs[newIndex].onGrabFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// 选择变动
|
// 选择变动
|
||||||
tabbedPane.addChangeListener {
|
tabbedPane.addChangeListener {
|
||||||
@@ -113,35 +92,6 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 快捷键
|
|
||||||
val inputMap = getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
|
||||||
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
|
|
||||||
val tabIndex = i - KeyEvent.VK_1 + 1
|
|
||||||
val actionKey = "select_$tabIndex"
|
|
||||||
actionMap.put(actionKey, object : AnAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
tabbedPane.selectedIndex = if (i == KeyEvent.VK_9 || tabIndex > tabbedPane.tabCount) {
|
|
||||||
tabbedPane.tabCount - 1
|
|
||||||
} else {
|
|
||||||
tabIndex - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
inputMap.put(KeyStroke.getKeyStroke(i, toolkit.menuShortcutKeyMaskEx), actionKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭 tab
|
|
||||||
actionMap.put("closeTab", object : AnAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
if (tabbedPane.selectedIndex >= 0) {
|
|
||||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabbedPane.selectedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, toolkit.menuShortcutKeyMaskEx), "closeTab")
|
|
||||||
|
|
||||||
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
tabbedPane.addMouseListener(object : MouseAdapter() {
|
tabbedPane.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
@@ -170,11 +120,13 @@ class TerminalTabbed(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 注册全局搜索
|
// 注册全局搜索
|
||||||
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
|
FindEverywhereProvider.getFindEverywhereProviders(windowScope)
|
||||||
|
.add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||||
val results = mutableListOf<FindEverywhereResult>()
|
val results = mutableListOf<FindEverywhereResult>()
|
||||||
for (i in 0 until tabbedPane.tabCount) {
|
for (i in 0 until tabbedPane.tabCount) {
|
||||||
if (tabbedPane.getComponentAt(i) is WelcomePanel) {
|
val c = tabbedPane.getComponentAt(i)
|
||||||
|
if (c is WelcomePanel || c is TransportPanel) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
results.add(
|
results.add(
|
||||||
@@ -198,15 +150,8 @@ class TerminalTabbed(
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
// 打开 Host
|
// 监听全局事件
|
||||||
ActionManager.getInstance().addAction(Actions.OPEN_HOST, object : AbstractAction() {
|
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
if (e !is OpenHostActionEvent) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
openHost(e.host)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,32 +183,25 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun openHost(host: Host) {
|
|
||||||
val tab = if (host.protocol == Protocol.SSH) SSHTerminalTab(host) else LocalTerminalTab(host)
|
|
||||||
addTab(tab)
|
|
||||||
tab.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
||||||
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
||||||
|
val tab = tabs[tabIndex]
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
val popupMenu = FlatPopupMenu()
|
||||||
|
|
||||||
// 修改名称
|
// 修改名称
|
||||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
|
||||||
val dialog = InputDialog(
|
val dialog = InputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
title = rename.text,
|
title = rename.text,
|
||||||
text = tabbedPane.getTitleAt(index),
|
text = tabbedPane.getTitleAt(tabIndex),
|
||||||
)
|
)
|
||||||
val text = dialog.getText()
|
val text = dialog.getText()
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
tabbedPane.setTitleAt(index, text)
|
tabbedPane.setTitleAt(tabIndex, text)
|
||||||
|
c.putClientProperty(titleProperty, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,35 +209,34 @@ class TerminalTabbed(
|
|||||||
|
|
||||||
// 克隆
|
// 克隆
|
||||||
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
|
||||||
clone.addActionListener {
|
clone.addActionListener { evt ->
|
||||||
val index = tabbedPane.selectedIndex
|
|
||||||
if (index > 0) {
|
|
||||||
val tab = tabs[index]
|
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
ActionManager.getInstance()
|
actionManager
|
||||||
.getAction(Actions.OPEN_HOST)
|
.getAction(OpenHostAction.OPEN_HOST)
|
||||||
.actionPerformed(OpenHostActionEvent(this, tab.host))
|
.actionPerformed(OpenHostActionEvent(this, tab.host, evt))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在新窗口中打开
|
// 在新窗口中打开
|
||||||
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
|
val openInNewWindow = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.open-in-new-window"))
|
||||||
openInNewWindow.addActionListener {
|
openInNewWindow.addActionListener(object : AnAction() {
|
||||||
val index = tabbedPane.selectedIndex
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (index > 0) {
|
val owner = evt.getData(DataProviders.TermoraFrame) ?: return
|
||||||
val tab = tabs[index]
|
if (tabIndex > 0) {
|
||||||
removeTabAt(index, false)
|
val title = tabbedPane.getTitleAt(tabIndex)
|
||||||
|
removeTabAt(tabIndex, false)
|
||||||
val dialog = TerminalTabDialog(
|
val dialog = TerminalTabDialog(
|
||||||
owner = SwingUtilities.getWindowAncestor(this),
|
owner = owner,
|
||||||
terminalTab = tab,
|
terminalTab = tab,
|
||||||
size = Dimension(min(size.width, 1280), min(size.height, 800))
|
size = Dimension(min(size.width, 1280), min(size.height, 800))
|
||||||
)
|
)
|
||||||
|
dialog.title = title
|
||||||
Disposer.register(dialog, tab)
|
Disposer.register(dialog, tab)
|
||||||
Disposer.register(this, dialog)
|
Disposer.register(this@TerminalTabbed, dialog)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
|
|
||||||
@@ -332,19 +269,17 @@ class TerminalTabbed(
|
|||||||
clone.isEnabled = close.isEnabled
|
clone.isEnabled = close.isEnabled
|
||||||
openInNewWindow.isEnabled = close.isEnabled
|
openInNewWindow.isEnabled = close.isEnabled
|
||||||
|
|
||||||
// SFTP不允许克隆
|
// 如果不允许克隆
|
||||||
if (clone.isEnabled && getSelectedTerminalTab() is SFTPTerminalTab) {
|
if (clone.isEnabled && !tab.canClone()) {
|
||||||
clone.isEnabled = false
|
clone.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (close.isEnabled) {
|
if (close.isEnabled) {
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||||
reconnect.addActionListener {
|
reconnect.addActionListener {
|
||||||
val index = tabbedPane.selectedIndex
|
if (tabIndex > 0) {
|
||||||
if (index > 0) {
|
tabs[tabIndex].reconnect()
|
||||||
tabs[index].reconnect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,21 +290,85 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun addTab(tab: TerminalTab) {
|
private fun addTab(index: Int, tab: TerminalTab) {
|
||||||
tabbedPane.addTab(
|
val c = tab.getJComponent()
|
||||||
tab.getTitle(),
|
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||||
|
|
||||||
|
tabbedPane.insertTab(
|
||||||
|
title,
|
||||||
tab.getIcon(),
|
tab.getIcon(),
|
||||||
tab.getJComponent()
|
c,
|
||||||
|
StringUtils.EMPTY,
|
||||||
|
index
|
||||||
)
|
)
|
||||||
|
c.putClientProperty(titleProperty, title)
|
||||||
|
|
||||||
// 监听 icons 变化
|
// 监听 icons 变化
|
||||||
tab.addPropertyChangeListener(iconListener)
|
tab.addPropertyChangeListener(iconListener)
|
||||||
|
|
||||||
tabs.add(tab)
|
tabs.add(index, tab)
|
||||||
tabbedPane.selectedIndex = tabbedPane.tabCount - 1
|
tabbedPane.selectedIndex = index
|
||||||
Disposer.register(this, tab)
|
Disposer.register(this, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对着 ToolBar 右键
|
||||||
|
*/
|
||||||
|
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
||||||
|
init {
|
||||||
|
Disposer.register(this@TerminalTabbed, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eventDispatched(event: AWTEvent) {
|
||||||
|
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED || !SwingUtilities.isRightMouseButton(event)) return
|
||||||
|
// 如果 ToolBar 没有显示
|
||||||
|
if (!toolbar.isShowing) return
|
||||||
|
// 如果不是作用于在 ToolBar 上面
|
||||||
|
if (!Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen)) return
|
||||||
|
|
||||||
|
// 显示右键菜单
|
||||||
|
showContextMenu(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showContextMenu(event: MouseEvent) {
|
||||||
|
val popupMenu = FlatPopupMenu()
|
||||||
|
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||||
|
val dialog = CustomizeToolBarDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(this@TerminalTabbed),
|
||||||
|
termoraToolBar
|
||||||
|
)
|
||||||
|
if (dialog.open()) {
|
||||||
|
termoraToolBar.rebuild()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupMenu.show(event.component, event.x, event.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
toolkit.removeAWTEventListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*private inner class CustomizeToolBarDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
isModal = true
|
||||||
|
title = I18n.getString("termora.setting")
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
val model = DefaultListModel<String>()
|
||||||
|
val checkBoxList = CheckBoxList(model)
|
||||||
|
checkBoxList.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||||
|
model.addElement("Test")
|
||||||
|
return checkBoxList
|
||||||
|
}
|
||||||
|
|
||||||
|
}*/
|
||||||
|
|
||||||
private inner class SwitchFindEverywhereResult(
|
private inner class SwitchFindEverywhereResult(
|
||||||
private val title: String,
|
private val title: String,
|
||||||
private val icon: Icon?,
|
private val icon: Icon?,
|
||||||
@@ -401,7 +400,11 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addTerminalTab(tab: TerminalTab) {
|
override fun addTerminalTab(tab: TerminalTab) {
|
||||||
addTab(tab)
|
addTab(tabs.size, tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTerminalTab(index: Int, tab: TerminalTab) {
|
||||||
|
addTab(index, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSelectedTerminalTab(): TerminalTab? {
|
override fun getSelectedTerminalTab(): TerminalTab? {
|
||||||
@@ -426,5 +429,24 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) {
|
||||||
|
for (i in 0 until tabs.size) {
|
||||||
|
if (tabs[i] == tab) {
|
||||||
|
removeTabAt(i, disposable)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == DataProviders.TerminalTab) {
|
||||||
|
dataProviderSupport.removeData(dataKey)
|
||||||
|
if (tabbedPane.selectedIndex >= 0 && tabs.size > tabbedPane.selectedIndex) {
|
||||||
|
dataProviderSupport.addData(dataKey, tabs[tabbedPane.selectedIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataProviderSupport.getData(dataKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ package app.termora
|
|||||||
|
|
||||||
interface TerminalTabbedManager {
|
interface TerminalTabbedManager {
|
||||||
fun addTerminalTab(tab: TerminalTab)
|
fun addTerminalTab(tab: TerminalTab)
|
||||||
|
fun addTerminalTab(index: Int, tab: TerminalTab)
|
||||||
fun getSelectedTerminalTab(): TerminalTab?
|
fun getSelectedTerminalTab(): TerminalTab?
|
||||||
fun getTerminalTabs(): List<TerminalTab>
|
fun getTerminalTabs(): List<TerminalTab>
|
||||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||||
|
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||||
}
|
}
|
||||||
@@ -1,117 +1,57 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.findeverywhere.FindEverywhere
|
|
||||||
import app.termora.highlight.KeywordHighlightDialog
|
import app.termora.actions.ActionManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.macro.MacroAction
|
import app.termora.actions.DataProviderSupport
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.FlatDesktop
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import io.github.g00fy2.versioncompare.Version
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.swing.Swing
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.jdesktop.swingx.JXEditorPane
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
import java.awt.KeyEventDispatcher
|
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.event.*
|
import java.awt.event.MouseAdapter
|
||||||
import java.net.URI
|
import java.awt.event.MouseEvent
|
||||||
|
import java.util.*
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.*
|
import javax.swing.Box
|
||||||
|
import javax.swing.JFrame
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||||
import javax.swing.event.HyperlinkEvent
|
import javax.swing.UIManager
|
||||||
import kotlin.concurrent.fixedRateTimer
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.system.exitProcess
|
|
||||||
import kotlin.time.Duration.Companion.hours
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
|
|
||||||
fun assertEventDispatchThread() {
|
fun assertEventDispatchThread() {
|
||||||
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TermoraFrame : JFrame() {
|
class TermoraFrame : JFrame(), DataProvider {
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(TermoraFrame::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val toolbar = JToolBar()
|
private val actionManager get() = ActionManager.getInstance()
|
||||||
private val tabbedPane = MyTabbedPane()
|
private val id = UUID.randomUUID().toString()
|
||||||
private lateinit var terminalTabbed: TerminalTabbed
|
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||||
private val disposable = Disposer.newDisposable()
|
|
||||||
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
|
||||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
||||||
private val updaterManager get() = UpdaterManager.instance
|
private val tabbedPane = MyTabbedPane()
|
||||||
|
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
|
||||||
|
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
||||||
|
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
||||||
|
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.isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initActions()
|
|
||||||
initView()
|
initView()
|
||||||
initEvents()
|
initEvents()
|
||||||
initDesktopHandler()
|
|
||||||
scheduleUpdate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
|
||||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
|
||||||
addComponentListener(object : ComponentAdapter() {
|
|
||||||
override fun componentResized(e: ComponentEvent) {
|
|
||||||
if (SystemInfo.isMacOS) {
|
|
||||||
val left = titleBar.leftInset.toInt()
|
|
||||||
if (tabbedPane.tabAreaInsets.left != left) {
|
|
||||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
|
||||||
}
|
|
||||||
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
|
||||||
|
|
||||||
val right = titleBar.rightInset.toInt()
|
|
||||||
|
|
||||||
for (i in 0 until toolbar.componentCount) {
|
|
||||||
val c = toolbar.getComponent(i)
|
|
||||||
if (c.name == "spacing") {
|
|
||||||
if (c.width == right) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toolbar.remove(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (right > 0) {
|
|
||||||
val spacing = Box.createHorizontalStrut(right)
|
|
||||||
spacing.name = "spacing"
|
|
||||||
toolbar.add(spacing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
forceHitTest()
|
forceHitTest()
|
||||||
|
|
||||||
// macos 需要判断是否全部删除
|
// macos 需要判断是否全部删除
|
||||||
@@ -126,146 +66,19 @@ class TermoraFrame : JFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// global shortcuts
|
|
||||||
rootPane.actionMap.put(Actions.FIND_EVERYWHERE, ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE))
|
|
||||||
rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
|
||||||
.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, toolkit.menuShortcutKeyMaskEx), Actions.FIND_EVERYWHERE)
|
|
||||||
|
|
||||||
// double shift
|
|
||||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(object : KeyEventDispatcher {
|
|
||||||
private var lastTime = -1L
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
|
||||||
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
if (now - 250 < lastTime) {
|
|
||||||
ActionManager.getInstance().getAction(Actions.FIND_EVERYWHERE)
|
|
||||||
.actionPerformed(ActionEvent(rootPane, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
|
||||||
}
|
|
||||||
lastTime = now
|
|
||||||
} else if (e.keyCode != KeyEvent.VK_SHIFT) { // 如果不是 Shift 键,那么就阻断了连续性,重置时间
|
|
||||||
lastTime = -1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听主题变化 需要动态修改控制栏颜色
|
// 监听主题变化 需要动态修改控制栏颜色
|
||||||
if (SystemInfo.isWindows && isWindowDecorationsSupported) {
|
if (SystemInfo.isWindows && isWindowDecorationsSupported) {
|
||||||
ThemeManager.instance.addThemeChangeListener(object : ThemeChangeListener {
|
ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// dispose
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowClosed(e: WindowEvent) {
|
|
||||||
|
|
||||||
Disposer.dispose(disposable)
|
|
||||||
Disposer.dispose(ApplicationDisposable.instance)
|
|
||||||
|
|
||||||
try {
|
|
||||||
Disposer.getTree().assertIsEmpty(true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error(e.message)
|
|
||||||
}
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initActions() {
|
|
||||||
// SETTING
|
|
||||||
ActionManager.getInstance().addAction(Actions.SETTING, object : AnAction(
|
|
||||||
I18n.getString("termora.setting"),
|
|
||||||
Icons.settings
|
|
||||||
) {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
preferencesHandler.run()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// MULTIPLE
|
|
||||||
ActionManager.getInstance().addAction(Actions.MULTIPLE, object : AnAction(
|
|
||||||
I18n.getString("termora.tools.multiple"),
|
|
||||||
Icons.vcs
|
|
||||||
) {
|
|
||||||
init {
|
|
||||||
setStateAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun actionPerformed(evt: ActionEvent) {
|
|
||||||
TerminalPanelFactory.instance.repaintAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// Keyword Highlight
|
|
||||||
ActionManager.getInstance().addAction(Actions.KEYWORD_HIGHLIGHT_EVERYWHERE, 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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
FindEverywhere(frame).isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Key manager
|
|
||||||
ActionManager.getInstance().addAction(Actions.KEY_MANAGER, object : AnAction(
|
|
||||||
I18n.getString("termora.keymgr.title"),
|
|
||||||
Icons.greyKey
|
|
||||||
) {
|
|
||||||
override fun actionPerformed(evt: ActionEvent) {
|
|
||||||
if (this.isEnabled) {
|
|
||||||
KeyManagerDialog(this@TermoraFrame).isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
if (isWindowDecorationsSupported) {
|
if (isWindowDecorationsSupported) {
|
||||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
||||||
@@ -288,10 +101,7 @@ class TermoraFrame : JFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
minimumSize = Dimension(640, 400)
|
minimumSize = Dimension(640, 400)
|
||||||
terminalTabbed = TerminalTabbed(toolbar, tabbedPane).apply {
|
terminalTabbed.addTerminalTab(welcomePanel)
|
||||||
Application.registerService(TerminalTabbedManager::class, this)
|
|
||||||
}
|
|
||||||
terminalTabbed.addTab(WelcomePanel())
|
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
// macOS 要避开左边的控制栏
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
@@ -303,89 +113,13 @@ class TermoraFrame : JFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Disposer.register(disposable, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed)
|
add(terminalTabbed)
|
||||||
|
|
||||||
|
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
||||||
|
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUpdateDialog() {
|
|
||||||
val lastVersion = updaterManager.lastVersion
|
|
||||||
val editorPane = JXEditorPane()
|
|
||||||
editorPane.contentType = "text/html"
|
|
||||||
editorPane.text = lastVersion.htmlBody
|
|
||||||
editorPane.isEditable = false
|
|
||||||
editorPane.addHyperlinkListener {
|
|
||||||
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
|
||||||
Application.browse(it.url.toURI())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editorPane.background = DynamicColor("window")
|
|
||||||
val scrollPane = JScrollPane(editorPane)
|
|
||||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
|
||||||
scrollPane.preferredSize = Dimension(
|
|
||||||
UIManager.getInt("Dialog.width") - 100,
|
|
||||||
UIManager.getInt("Dialog.height") - 100
|
|
||||||
)
|
|
||||||
|
|
||||||
val option = OptionPane.showConfirmDialog(
|
|
||||||
this,
|
|
||||||
scrollPane,
|
|
||||||
title = I18n.getString("termora.update.title"),
|
|
||||||
messageType = JOptionPane.PLAIN_MESSAGE,
|
|
||||||
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
|
|
||||||
options = arrayOf(
|
|
||||||
I18n.getString("termora.update.update"),
|
|
||||||
I18n.getString("termora.update.ignore"),
|
|
||||||
I18n.getString("termora.cancel")
|
|
||||||
),
|
|
||||||
initialValue = I18n.getString("termora.update.update")
|
|
||||||
)
|
|
||||||
if (option == JOptionPane.CANCEL_OPTION) {
|
|
||||||
return
|
|
||||||
} else if (option == JOptionPane.NO_OPTION) {
|
|
||||||
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
|
|
||||||
updaterManager.ignore(updaterManager.lastVersion.version)
|
|
||||||
} else if (option == JOptionPane.YES_OPTION) {
|
|
||||||
ActionManager.getInstance()
|
|
||||||
.setEnabled(Actions.APP_UPDATE, false)
|
|
||||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
private fun scheduleUpdate() {
|
|
||||||
fixedRateTimer(
|
|
||||||
name = "check-update-timer",
|
|
||||||
initialDelay = 3.minutes.inWholeMilliseconds,
|
|
||||||
period = 5.hours.inWholeMilliseconds, daemon = true
|
|
||||||
) {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun checkUpdate() {
|
|
||||||
|
|
||||||
val latestVersion = updaterManager.fetchLatestVersion()
|
|
||||||
if (latestVersion.isSelf) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val newVersion = Version(latestVersion.version)
|
|
||||||
val version = Version(Application.getVersion())
|
|
||||||
if (newVersion <= version) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updaterManager.isIgnored(latestVersion.version)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
ActionManager.getInstance()
|
|
||||||
.setEnabled(Actions.APP_UPDATE, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun forceHitTest() {
|
private fun forceHitTest() {
|
||||||
val mouseAdapter = object : MouseAdapter() {
|
val mouseAdapter = object : MouseAdapter() {
|
||||||
@@ -405,7 +139,7 @@ class TermoraFrame : JFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
override fun mousePressed(e: MouseEvent) {
|
||||||
if (e.source == toolbar) {
|
if (e.source == toolbar.getJToolBar()) {
|
||||||
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
|
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
|
||||||
if (JBR.isWindowMoveSupported()) {
|
if (JBR.isWindowMoveSupported()) {
|
||||||
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
|
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
|
||||||
@@ -440,15 +174,29 @@ class TermoraFrame : JFrame() {
|
|||||||
tabbedPane.addMouseListener(mouseAdapter)
|
tabbedPane.addMouseListener(mouseAdapter)
|
||||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
tabbedPane.addMouseMotionListener(mouseAdapter)
|
||||||
|
|
||||||
toolbar.addMouseListener(mouseAdapter)
|
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||||
toolbar.addMouseMotionListener(mouseAdapter)
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initDesktopHandler() {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
if (SystemInfo.isMacOS) {
|
return dataProviderSupport.getData(dataKey)
|
||||||
FlatDesktop.setPreferencesHandler {
|
?: terminalTabbed.getData(dataKey)
|
||||||
preferencesHandler.run()
|
?: welcomePanel.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as TermoraFrame
|
||||||
|
|
||||||
|
return id == other.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return id.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
60
src/main/kotlin/app/termora/TermoraFrameManager.kt
Normal file
60
src/main/kotlin/app/termora/TermoraFrameManager.kt
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class TermoraFrameManager {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(TermoraFrameManager::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): TermoraFrameManager {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(TermoraFrameManager::class) { TermoraFrameManager() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createWindow(): TermoraFrame {
|
||||||
|
val frame = TermoraFrame()
|
||||||
|
registerCloseCallback(frame)
|
||||||
|
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||||
|
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
|
||||||
|
frame.setSize(1280, 800)
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun registerCloseCallback(window: TermoraFrame) {
|
||||||
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
|
||||||
|
// dispose windowScope
|
||||||
|
Disposer.dispose(ApplicationScope.forWindowScope(e.window))
|
||||||
|
|
||||||
|
val windowScopes = ApplicationScope.windowScopes()
|
||||||
|
|
||||||
|
// 如果已经没有 Window 域了,那么就可以退出程序了
|
||||||
|
if (windowScopes.isEmpty()) {
|
||||||
|
this@TermoraFrameManager.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dispose() {
|
||||||
|
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||||
|
|
||||||
|
try {
|
||||||
|
Disposer.getTree().assertIsEmpty(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
177
src/main/kotlin/app/termora/TermoraToolBar.kt
Normal file
177
src/main/kotlin/app/termora/TermoraToolBar.kt
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
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 java.awt.Insets
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import javax.swing.Box
|
||||||
|
import javax.swing.JToolBar
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ToolBarAction(
|
||||||
|
val id: String,
|
||||||
|
val visible: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
class TermoraToolBar(
|
||||||
|
private val titleBar: WindowDecorations.CustomTitleBar,
|
||||||
|
private val tabbedPane: FlatTabbedPane
|
||||||
|
) {
|
||||||
|
private val properties by lazy { Database.getDatabase().properties }
|
||||||
|
private val toolbar by lazy { MyToolBar().apply { rebuild(this) } }
|
||||||
|
|
||||||
|
|
||||||
|
fun getJToolBar(): JToolBar {
|
||||||
|
return toolbar
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取到所有的 Action
|
||||||
|
*/
|
||||||
|
fun getAllActions(): List<ToolBarAction> {
|
||||||
|
return listOf(
|
||||||
|
ToolBarAction(Actions.SFTP, true),
|
||||||
|
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||||
|
ToolBarAction(Actions.MACRO, true),
|
||||||
|
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||||
|
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||||
|
ToolBarAction(Actions.MULTIPLE, true),
|
||||||
|
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||||
|
ToolBarAction(SettingsAction.SETTING, true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取到所有 Action,会根据用户个性化排序/显示
|
||||||
|
*/
|
||||||
|
fun getActions(): List<ToolBarAction> {
|
||||||
|
val text = properties.getString(
|
||||||
|
"Termora.ToolBar.Actions",
|
||||||
|
StringUtils.EMPTY
|
||||||
|
)
|
||||||
|
|
||||||
|
val actions = getAllActions()
|
||||||
|
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储的 action
|
||||||
|
val storageActions = (ohMyJson.runCatching {
|
||||||
|
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
|
||||||
|
}.getOrNull() ?: return actions).toMutableList()
|
||||||
|
|
||||||
|
for (action in actions) {
|
||||||
|
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
|
||||||
|
if (storageActions.none { it.id == action.id }) {
|
||||||
|
storageActions.addFirst(ToolBarAction(action.id, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果存储的 Action 在所有 Action 里没有,那么移除
|
||||||
|
storageActions.removeIf { e -> actions.none { e.id == it.id } }
|
||||||
|
|
||||||
|
return storageActions
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rebuild() {
|
||||||
|
rebuild(this.toolbar)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rebuild(toolbar: JToolBar) {
|
||||||
|
val actionManager = ActionManager.getInstance()
|
||||||
|
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||||
|
|
||||||
|
toolbar.removeAll()
|
||||||
|
|
||||||
|
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEnabled(): Boolean {
|
||||||
|
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
|
||||||
|
// update btn
|
||||||
|
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||||
|
updateBtn.isVisible = updateBtn.isEnabled
|
||||||
|
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||||
|
toolbar.add(updateBtn)
|
||||||
|
|
||||||
|
|
||||||
|
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||||
|
for (action in getActions()) {
|
||||||
|
if (action.visible) {
|
||||||
|
actionManager.getAction(action.id)?.let {
|
||||||
|
toolbar.add(actionContainerFactory.createButton(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (toolbar is MyToolBar) {
|
||||||
|
toolbar.adjust()
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.revalidate()
|
||||||
|
toolbar.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MyToolBar : JToolBar() {
|
||||||
|
init {
|
||||||
|
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
adjust()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun adjust() {
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
val left = titleBar.leftInset.toInt()
|
||||||
|
if (tabbedPane.tabAreaInsets.left != left) {
|
||||||
|
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
||||||
|
}
|
||||||
|
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
|
||||||
|
val right = titleBar.rightInset.toInt()
|
||||||
|
val toolbar = this@MyToolBar
|
||||||
|
for (i in 0 until toolbar.componentCount) {
|
||||||
|
val c = toolbar.getComponent(i)
|
||||||
|
if (c.name == "spacing") {
|
||||||
|
if (c.width == right) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolbar.remove(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right > 0) {
|
||||||
|
val spacing = Box.createHorizontalStrut(right)
|
||||||
|
spacing.name = "spacing"
|
||||||
|
toolbar.add(spacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.*
|
import com.formdev.flatlaf.extras.components.*
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.event.FocusAdapter
|
import java.awt.event.*
|
||||||
import java.awt.event.FocusEvent
|
|
||||||
import java.awt.event.KeyAdapter
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import javax.swing.DefaultListCellRenderer
|
import javax.swing.DefaultListCellRenderer
|
||||||
import javax.swing.JComboBox
|
import javax.swing.JComboBox
|
||||||
@@ -53,6 +51,15 @@ class OutlineTextArea : FlatTextArea() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OutlineComboBox<T> : JComboBox<T>() {
|
||||||
|
init {
|
||||||
|
addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
putClientProperty(FlatClientProperties.OUTLINE, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class FixedLengthTextArea(var maxLength: Int = Int.MAX_VALUE) : FlatTextArea() {
|
class FixedLengthTextArea(var maxLength: Int = Int.MAX_VALUE) : FlatTextArea() {
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.FlatAnimatedLafChange
|
import com.formdev.flatlaf.extras.FlatAnimatedLafChange
|
||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
@@ -24,12 +23,16 @@ class ThemeManager private constructor() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(ThemeManager::class.java)
|
private val log = LoggerFactory.getLogger(ThemeManager::class.java)
|
||||||
val instance by lazy { ThemeManager() }
|
fun getInstance(): ThemeManager {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(ThemeManager::class) { ThemeManager() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appearance by lazy { Database.getDatabase().appearance }
|
||||||
val themes = mapOf(
|
val themes = mapOf(
|
||||||
"Light" to LightLaf::class.java.name,
|
"Light" to LightLaf::class.java.name,
|
||||||
"Dark" to DarkLaf::class.java.name,
|
"Dark" to DarkLaf::class.java.name,
|
||||||
|
"Dracula" to DraculaLaf::class.java.name,
|
||||||
"iTerm2 Dark" to iTerm2DarkLaf::class.java.name,
|
"iTerm2 Dark" to iTerm2DarkLaf::class.java.name,
|
||||||
"Termius Dark" to TermiusDarkLaf::class.java.name,
|
"Termius Dark" to TermiusDarkLaf::class.java.name,
|
||||||
"Termius Light" to TermiusLightLaf::class.java.name,
|
"Termius Light" to TermiusLightLaf::class.java.name,
|
||||||
@@ -77,18 +80,16 @@ class ThemeManager private constructor() {
|
|||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
|
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
|
||||||
override fun accept(isDark: Boolean) {
|
override fun accept(isDark: Boolean) {
|
||||||
if (!Database.instance.appearance.followSystem) {
|
if (!appearance.followSystem) {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FlatLaf.isLafDark() && isDark) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
if (isDark) {
|
if (isDark) {
|
||||||
SwingUtilities.invokeLater { change("Dark") }
|
change(appearance.darkTheme)
|
||||||
} else {
|
} else {
|
||||||
SwingUtilities.invokeLater { change("Light") }
|
change(appearance.lightTheme)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.db.Database
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -19,7 +18,9 @@ import java.util.*
|
|||||||
class UpdaterManager private constructor() {
|
class UpdaterManager private constructor() {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(UpdaterManager::class.java)
|
private val log = LoggerFactory.getLogger(UpdaterManager::class.java)
|
||||||
val instance by lazy { UpdaterManager() }
|
fun getInstance(): UpdaterManager {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(UpdaterManager::class) { UpdaterManager() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Asset(
|
data class Asset(
|
||||||
@@ -58,7 +59,7 @@ class UpdaterManager private constructor() {
|
|||||||
val isSelf get() = this == self
|
val isSelf get() = this == self
|
||||||
}
|
}
|
||||||
|
|
||||||
private val properties get() = Database.instance.properties
|
private val properties get() = Database.getDatabase().properties
|
||||||
var lastVersion = LatestVersion.self
|
var lastVersion = LatestVersion.self
|
||||||
|
|
||||||
fun fetchLatestVersion(): LatestVersion {
|
fun fetchLatestVersion(): LatestVersion {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.db.Database
|
|
||||||
|
import app.termora.actions.*
|
||||||
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhere
|
|
||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
@@ -19,17 +20,18 @@ import java.awt.event.ComponentAdapter
|
|||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.DocumentEvent
|
import javax.swing.event.DocumentEvent
|
||||||
import javax.swing.tree.TreePath
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
|
class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()), Disposable, TerminalTab,
|
||||||
private val properties get() = Database.instance.properties
|
DataProvider {
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
private val rootPanel = JPanel(BorderLayout())
|
private val rootPanel = JPanel(BorderLayout())
|
||||||
private val searchTextField = FlatTextField()
|
private val searchTextField = FlatTextField()
|
||||||
private val hostTree = HostTree()
|
private val hostTree = HostTree()
|
||||||
private val bannerPanel = BannerPanel()
|
private val bannerPanel = BannerPanel()
|
||||||
private val toggle = FlatButton()
|
private val toggle = FlatButton()
|
||||||
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
|
private var fullContent = properties.getString("WelcomeFullContent", "false").toBoolean()
|
||||||
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -51,6 +53,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
|
|||||||
rootPanel.add(panel, BorderLayout.CENTER)
|
rootPanel.add(panel, BorderLayout.CENTER)
|
||||||
add(rootPanel, BorderLayout.CENTER)
|
add(rootPanel, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
dataProviderSupport.addData(DataProviders.Welcome.HostTree, hostTree)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
|
|||||||
newHost.isFocusable = false
|
newHost.isFocusable = false
|
||||||
newHost.buttonType = FlatButton.ButtonType.toolBarButton
|
newHost.buttonType = FlatButton.ButtonType.toolBarButton
|
||||||
newHost.addActionListener { e ->
|
newHost.addActionListener { e ->
|
||||||
ActionManager.getInstance().getAction(Actions.ADD_HOST)?.actionPerformed(e)
|
ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)?.actionPerformed(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -117,7 +120,7 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
|
|||||||
private fun createHostPanel(): JComponent {
|
private fun createHostPanel(): JComponent {
|
||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
hostTree.actionMap.put("find", object : AnAction() {
|
hostTree.actionMap.put("find", object : AnAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
searchTextField.requestFocusInWindow()
|
searchTextField.requestFocusInWindow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -160,16 +163,8 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
ActionManager.getInstance().addAction(Actions.ADD_HOST, object : AnAction() {
|
FindEverywhereProvider.getFindEverywhereProviders(windowScope)
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
.add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
|
||||||
if (hostTree.selectionCount < 1) {
|
|
||||||
hostTree.selectionPath = TreePath(hostTree.model.root)
|
|
||||||
}
|
|
||||||
hostTree.showAddHostDialog()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
FindEverywhere.registerProvider(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
|
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||||
return TreeUtils.children(hostTree.model, hostTree.model.root)
|
return TreeUtils.children(hostTree.model, hostTree.model.root)
|
||||||
.filterIsInstance<Host>()
|
.filterIsInstance<Host>()
|
||||||
@@ -233,15 +228,28 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun canReconnect(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canClose(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canClone(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
|
hostTree.setModel(null)
|
||||||
properties.putString("WelcomeFullContent", fullContent.toString())
|
properties.putString("WelcomeFullContent", fullContent.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
ActionManager.getInstance()
|
ActionManager.getInstance()
|
||||||
.getAction(Actions.OPEN_HOST)
|
.getAction(OpenHostAction.OPEN_HOST)
|
||||||
?.actionPerformed(OpenHostActionEvent(this, host))
|
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -258,5 +266,9 @@ class WelcomePanel : JPanel(BorderLayout()), Disposable, TerminalTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
return dataProviderSupport.getData(dataKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
72
src/main/kotlin/app/termora/actions/ActionManager.kt
Normal file
72
src/main/kotlin/app/termora/actions/ActionManager.kt
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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(TabReconnectAction.RECONNECT_TAB, TabReconnectAction())
|
||||||
|
addAction(SettingsAction.SETTING, SettingsAction())
|
||||||
|
|
||||||
|
addAction(NewHostAction.NEW_HOST, NewHostAction())
|
||||||
|
addAction(OpenHostAction.OPEN_HOST, OpenHostAction())
|
||||||
|
|
||||||
|
addAction(TerminalCopyAction.COPY, TerminalCopyAction())
|
||||||
|
addAction(TerminalPasteAction.PASTE, TerminalPasteAction())
|
||||||
|
addAction(TerminalFindAction.FIND, TerminalFindAction())
|
||||||
|
addAction(TerminalCloseAction.CLOSE, TerminalCloseAction())
|
||||||
|
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
|
||||||
|
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
|
||||||
|
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
|
||||||
|
|
||||||
|
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
|
||||||
|
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())
|
||||||
|
addAction(TerminalZoomResetAction.ZOOM_RESET, TerminalZoomResetAction())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAction(action: Action): Action {
|
||||||
|
val actionId = action.getValue(Action.ACTION_COMMAND_KEY) ?: throw IllegalArgumentException("Invalid action ID")
|
||||||
|
return addAction(actionId, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAction(id: Any, action: Action): Action {
|
||||||
|
if (getAction(id) != null) {
|
||||||
|
throw IllegalArgumentException("Action already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.addAction(id, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
src/main/kotlin/app/termora/actions/AnAction.kt
Normal file
30
src/main/kotlin/app/termora/actions/AnAction.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import org.jdesktop.swingx.action.BoundAction
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
abstract class AnAction : BoundAction {
|
||||||
|
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
constructor(icon: Icon) : super() {
|
||||||
|
super.putValue(SMALL_ICON, icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(name: String?) : super(name)
|
||||||
|
constructor(name: String?, icon: Icon?) : super(name, icon)
|
||||||
|
|
||||||
|
|
||||||
|
final override fun actionPerformed(evt: ActionEvent) {
|
||||||
|
if (evt is AnActionEvent) {
|
||||||
|
actionPerformed(evt)
|
||||||
|
} else {
|
||||||
|
actionPerformed(AnActionEvent(evt.source, evt.actionCommand, evt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected abstract fun actionPerformed(evt: AnActionEvent)
|
||||||
|
|
||||||
|
}
|
||||||
61
src/main/kotlin/app/termora/actions/AnActionEvent.kt
Normal file
61
src/main/kotlin/app/termora/actions/AnActionEvent.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
|
||||||
|
open class AnActionEvent(
|
||||||
|
source: Any, command: String,
|
||||||
|
val event: EventObject
|
||||||
|
) : ActionEvent(source, AN_ACTION_PERFORMED, command), DataProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val AN_ACTION_PERFORMED = ACTION_PERFORMED + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val window: Window
|
||||||
|
get() = getData(DataProviders.TermoraFrame)
|
||||||
|
?: KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||||
|
|
||||||
|
|
||||||
|
public override fun consume() {
|
||||||
|
super.consumed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun isConsumed(): Boolean {
|
||||||
|
return super.isConsumed()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
val source = getSource()
|
||||||
|
if (source !is Component) {
|
||||||
|
if (source is DataProvider) {
|
||||||
|
return source.getData(dataKey)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
var c = source as Component?
|
||||||
|
while (c != null) {
|
||||||
|
if (c is DataProvider) {
|
||||||
|
val data = c.getData(dataKey)
|
||||||
|
if (data != null) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val p = c.parent
|
||||||
|
c = if (p == null && c is JPopupMenu) {
|
||||||
|
c.invoker
|
||||||
|
} else {
|
||||||
|
p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/main/kotlin/app/termora/actions/AppUpdateAction.kt
Normal file
117
src/main/kotlin/app/termora/actions/AppUpdateAction.kt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import io.github.g00fy2.versioncompare.Version
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jdesktop.swingx.JXEditorPane
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.net.URI
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.JScrollPane
|
||||||
|
import javax.swing.UIManager
|
||||||
|
import javax.swing.event.HyperlinkEvent
|
||||||
|
import kotlin.concurrent.fixedRateTimer
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
class AppUpdateAction : AnAction(
|
||||||
|
StringUtils.EMPTY,
|
||||||
|
Icons.ideUpdate
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val updaterManager get() = UpdaterManager.getInstance()
|
||||||
|
|
||||||
|
init {
|
||||||
|
isEnabled = false
|
||||||
|
scheduleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
showUpdateDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
private fun scheduleUpdate() {
|
||||||
|
fixedRateTimer(
|
||||||
|
name = "check-update-timer",
|
||||||
|
initialDelay = 3.minutes.inWholeMilliseconds,
|
||||||
|
period = 5.hours.inWholeMilliseconds, daemon = true
|
||||||
|
) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkUpdate() {
|
||||||
|
|
||||||
|
val latestVersion = updaterManager.fetchLatestVersion()
|
||||||
|
if (latestVersion.isSelf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newVersion = Version(latestVersion.version)
|
||||||
|
val version = Version(Application.getVersion())
|
||||||
|
if (newVersion <= version) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updaterManager.isIgnored(latestVersion.version)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
ActionManager.getInstance()
|
||||||
|
.setEnabled(Actions.APP_UPDATE, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUpdateDialog() {
|
||||||
|
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||||
|
val lastVersion = updaterManager.lastVersion
|
||||||
|
val editorPane = JXEditorPane()
|
||||||
|
editorPane.contentType = "text/html"
|
||||||
|
editorPane.text = lastVersion.htmlBody
|
||||||
|
editorPane.isEditable = false
|
||||||
|
editorPane.addHyperlinkListener {
|
||||||
|
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
||||||
|
Application.browse(it.url.toURI())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editorPane.background = DynamicColor("window")
|
||||||
|
val scrollPane = JScrollPane(editorPane)
|
||||||
|
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||||
|
scrollPane.preferredSize = Dimension(
|
||||||
|
UIManager.getInt("Dialog.width") - 100,
|
||||||
|
UIManager.getInt("Dialog.height") - 100
|
||||||
|
)
|
||||||
|
|
||||||
|
val option = OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
scrollPane,
|
||||||
|
title = I18n.getString("termora.update.title"),
|
||||||
|
messageType = JOptionPane.PLAIN_MESSAGE,
|
||||||
|
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
|
||||||
|
options = arrayOf(
|
||||||
|
I18n.getString("termora.update.update"),
|
||||||
|
I18n.getString("termora.update.ignore"),
|
||||||
|
I18n.getString("termora.cancel")
|
||||||
|
),
|
||||||
|
initialValue = I18n.getString("termora.update.update")
|
||||||
|
)
|
||||||
|
if (option == JOptionPane.CANCEL_OPTION) {
|
||||||
|
return
|
||||||
|
} else if (option == JOptionPane.NO_OPTION) {
|
||||||
|
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
|
||||||
|
updaterManager.ignore(updaterManager.lastVersion.version)
|
||||||
|
} else if (option == JOptionPane.YES_OPTION) {
|
||||||
|
ActionManager.getInstance()
|
||||||
|
.setEnabled(Actions.APP_UPDATE, false)
|
||||||
|
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/kotlin/app/termora/actions/DataProvider.kt
Normal file
21
src/main/kotlin/app/termora/actions/DataProvider.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据提供者,从 [AnActionEvent.source] 开始搜索然后依次 [getData] 获取数据
|
||||||
|
*/
|
||||||
|
interface DataProvider {
|
||||||
|
companion object {
|
||||||
|
val EMPTY = object : DataProvider {
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据提供
|
||||||
|
*/
|
||||||
|
fun <T : Any> getData(dataKey: DataKey<T>): T?
|
||||||
|
}
|
||||||
24
src/main/kotlin/app/termora/actions/DataProviderSupport.kt
Normal file
24
src/main/kotlin/app/termora/actions/DataProviderSupport.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
|
||||||
|
class DataProviderSupport : DataProvider {
|
||||||
|
private val map = mutableMapOf<DataKey<*>, Any>()
|
||||||
|
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (map.containsKey(dataKey)) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return map[dataKey] as T
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Any> addData(key: DataKey<T>, data: T) {
|
||||||
|
map[key] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeData(key: DataKey<*>) {
|
||||||
|
map.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/kotlin/app/termora/actions/DataProviders.kt
Normal file
21
src/main/kotlin/app/termora/actions/DataProviders.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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 TerminalTab = DataKey(app.termora.TerminalTab::class)
|
||||||
|
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
||||||
|
|
||||||
|
val TermoraFrame = DataKey(app.termora.TermoraFrame::class)
|
||||||
|
val WindowScope = DataKey(app.termora.WindowScope::class)
|
||||||
|
|
||||||
|
|
||||||
|
object Welcome {
|
||||||
|
val HostTree = DataKey(app.termora.HostTree::class)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/kotlin/app/termora/actions/MultipleAction.kt
Normal file
17
src/main/kotlin/app/termora/actions/MultipleAction.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
|
||||||
|
class MultipleAction : AnAction(
|
||||||
|
I18n.getString("termora.tools.multiple"),
|
||||||
|
Icons.vcs
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
setStateAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
ApplicationScope.windowScopes().map { TerminalPanelFactory.getInstance(it) }
|
||||||
|
.forEach { it.repaintAll() }
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/kotlin/app/termora/actions/NewHostAction.kt
Normal file
46
src/main/kotlin/app/termora/actions/NewHostAction.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.HostDialog
|
||||||
|
import app.termora.HostManager
|
||||||
|
import app.termora.Protocol
|
||||||
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
|
class NewHostAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加主机对话框
|
||||||
|
*/
|
||||||
|
const val NEW_HOST = "NewHostAction"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
||||||
|
val model = tree.model
|
||||||
|
var lastHost = tree.lastSelectedPathComponent ?: model.root
|
||||||
|
if (lastHost !is Host) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastHost.protocol != Protocol.Folder) {
|
||||||
|
val p = model.getParent(lastHost) ?: return
|
||||||
|
lastHost = p
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = HostDialog(evt.window)
|
||||||
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
|
dialog.isVisible = true
|
||||||
|
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
||||||
|
|
||||||
|
hostManager.addHost(host)
|
||||||
|
|
||||||
|
tree.expandNode(lastHost)
|
||||||
|
|
||||||
|
tree.selectionPath = TreePath(model.getPathToRoot(host))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/kotlin/app/termora/actions/NewWindowAction.kt
Normal file
27
src/main/kotlin/app/termora/actions/NewWindowAction.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.TermoraFrameManager
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
|
||||||
|
class NewWindowAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开一个新的窗口
|
||||||
|
*/
|
||||||
|
const val NEW_WINDOW = "NewWindowAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-new-window"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, NEW_WINDOW)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||||
|
if (focusedWindow == evt.window) {
|
||||||
|
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/main/kotlin/app/termora/actions/OpenHostAction.kt
Normal file
28
src/main/kotlin/app/termora/actions/OpenHostAction.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.LocalTerminalTab
|
||||||
|
import app.termora.OpenHostActionEvent
|
||||||
|
import app.termora.Protocol
|
||||||
|
import app.termora.SSHTerminalTab
|
||||||
|
|
||||||
|
class OpenHostAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 打开一个主机
|
||||||
|
*/
|
||||||
|
const val OPEN_HOST = "OpenHostAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (evt !is OpenHostActionEvent) return
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
|
||||||
|
val tab = if (evt.host.protocol == Protocol.SSH)
|
||||||
|
SSHTerminalTab(windowScope, evt.host)
|
||||||
|
else LocalTerminalTab(windowScope, evt.host)
|
||||||
|
|
||||||
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
tab.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
|
||||||
|
class OpenLocalTerminalAction : AnAction(
|
||||||
|
I18n.getString("termora.find-everywhere.quick-command.local-terminal"),
|
||||||
|
Icons.terminal
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val LOCAL_TERMINAL = "OpenLocalTerminal"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-local-terminal"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, LOCAL_TERMINAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)?.actionPerformed(
|
||||||
|
OpenHostActionEvent(
|
||||||
|
evt.source,
|
||||||
|
Host(
|
||||||
|
name = name,
|
||||||
|
protocol = Protocol.Local
|
||||||
|
),
|
||||||
|
evt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
55
src/main/kotlin/app/termora/actions/SettingsAction.kt
Normal file
55
src/main/kotlin/app/termora/actions/SettingsAction.kt
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
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
|
||||||
|
// Doorman 的情况下不允许打开
|
||||||
|
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) {
|
||||||
|
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (isShowing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowing = true
|
||||||
|
|
||||||
|
val owner = evt.window
|
||||||
|
val dialog = SettingsDialog(owner)
|
||||||
|
dialog.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
this@SettingsAction.isShowing = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/kotlin/app/termora/actions/SwitchTabAction.kt
Normal file
36
src/main/kotlin/app/termora/actions/SwitchTabAction.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
|
||||||
|
class SwitchTabAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
const val SWITCH_TAB = "SwitchTabAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(ACTION_COMMAND_KEY, SWITCH_TAB)
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.switch-tab"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val original = evt.event
|
||||||
|
if (original !is KeyEvent) return
|
||||||
|
if (original.keyCode !in KeyEvent.VK_1..KeyEvent.VK_9) return
|
||||||
|
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val tabs = terminalTabbedManager.getTerminalTabs()
|
||||||
|
if (tabs.isEmpty()) return
|
||||||
|
|
||||||
|
|
||||||
|
val tabIndex = original.keyCode - KeyEvent.VK_1
|
||||||
|
if (tabIndex >= tabs.size) {
|
||||||
|
terminalTabbedManager.setSelectedTerminalTab(tabs.last())
|
||||||
|
} else {
|
||||||
|
terminalTabbedManager.setSelectedTerminalTab(tabs[tabIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.consume()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/kotlin/app/termora/actions/TabReconnectAction.kt
Normal file
21
src/main/kotlin/app/termora/actions/TabReconnectAction.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
|
||||||
|
class TabReconnectAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
const val RECONNECT_TAB = "TabReconnectAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(ACTION_COMMAND_KEY, RECONNECT_TAB)
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val tab = evt.getData(DataProviders.TerminalTab) ?: return
|
||||||
|
if (tab.canReconnect()) {
|
||||||
|
tab.reconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
class TerminalClearScreenAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
const val CLEAR_SCREEN = "ClearScreen"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, "Clear Terminal Buffer")
|
||||||
|
putValue(ACTION_COMMAND_KEY, CLEAR_SCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminal = evt.getData(DataProviders.Terminal) ?: return
|
||||||
|
terminal.getDocument().eraseInDisplay(3)
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
24
src/main/kotlin/app/termora/actions/TerminalCloseAction.kt
Normal file
24
src/main/kotlin/app/termora/actions/TerminalCloseAction.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
|
||||||
|
class TerminalCloseAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
const val CLOSE = "Close"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.close-tab"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, CLOSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
terminalTabbedManager.getSelectedTerminalTab()?.let {
|
||||||
|
terminalTabbedManager.closeTerminalTab(it)
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,26 +1,29 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.datatransfer.Transferable
|
import java.awt.datatransfer.Transferable
|
||||||
import java.awt.datatransfer.UnsupportedFlavorException
|
import java.awt.datatransfer.UnsupportedFlavorException
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import javax.swing.KeyStroke
|
|
||||||
|
|
||||||
class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAction(
|
class TerminalCopyAction : AnAction() {
|
||||||
KeyStroke.getKeyStroke(KeyEvent.VK_C, terminalPanel.toolkit.menuShortcutKeyMaskEx)
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val COPY = "TerminalCopy"
|
||||||
private val log = LoggerFactory.getLogger(TerminalCopyAction::class.java)
|
private val log = LoggerFactory.getLogger(TerminalCopyAction::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val systemClipboard get() = terminalPanel.toolkit.systemClipboard
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.copy-from-terminal"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, COPY)
|
||||||
|
}
|
||||||
|
|
||||||
override fun actionPerformed(e: KeyEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
val text = terminalPanel.copy()
|
val text = terminalPanel.copy()
|
||||||
|
val systemClipboard = terminalPanel.toolkit.systemClipboard
|
||||||
|
|
||||||
|
evt.consume()
|
||||||
|
|
||||||
// 如果文本为空,那么清空剪切板
|
// 如果文本为空,那么清空剪切板
|
||||||
if (text.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
@@ -31,16 +34,10 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAct
|
|||||||
systemClipboard.setContents(StringSelection(text), null)
|
systemClipboard.setContents(StringSelection(text), null)
|
||||||
terminalPanel.toast(I18n.getString("termora.terminal.copied"))
|
terminalPanel.toast(I18n.getString("termora.terminal.copied"))
|
||||||
if (log.isTraceEnabled) {
|
if (log.isTraceEnabled) {
|
||||||
log.info("Copy to clipboard. {}", text)
|
log.trace("Copy to clipboard. {}", text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun test(keyStroke: KeyStroke, e: KeyEvent): Boolean {
|
|
||||||
if (!SystemInfo.isMacOS) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return super.test(keyStroke, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class EmptyTransferable : Transferable {
|
private class EmptyTransferable : Transferable {
|
||||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||||
@@ -56,4 +53,5 @@ class TerminalCopyAction(private val terminalPanel: TerminalPanel) : TerminalAct
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
23
src/main/kotlin/app/termora/actions/TerminalFindAction.kt
Normal file
23
src/main/kotlin/app/termora/actions/TerminalFindAction.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
|
||||||
|
class TerminalFindAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
const val FIND = "TerminalFind"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-terminal-find"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, FIND)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
terminalPanel.showFind()
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
35
src/main/kotlin/app/termora/actions/TerminalPasteAction.kt
Normal file
35
src/main/kotlin/app/termora/actions/TerminalPasteAction.kt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
|
||||||
|
class TerminalPasteAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
const val PASTE = "TerminalPaste"
|
||||||
|
private val log = LoggerFactory.getLogger(TerminalPasteAction::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.paste-to-terminal"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, PASTE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
val systemClipboard = terminalPanel.toolkit.systemClipboard
|
||||||
|
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||||
|
val text = systemClipboard.getData(DataFlavor.stringFlavor)
|
||||||
|
if (text is String) {
|
||||||
|
terminalPanel.paste(text)
|
||||||
|
if (log.isTraceEnabled) {
|
||||||
|
log.info("Paste {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.terminal.Position
|
||||||
|
|
||||||
|
class TerminalSelectAllAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
const val SELECT_ALL = "TerminalSelectAll"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.select-all-in-terminal"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, SELECT_ALL)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val terminal = evt.getData(DataProviders.Terminal) ?: return
|
||||||
|
terminal.getSelectionModel().setSelection(
|
||||||
|
Position(y = 1, x = 1),
|
||||||
|
Position(y = terminal.getDocument().getLineCount(), x = terminal.getTerminalModel().getCols())
|
||||||
|
)
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/kotlin/app/termora/actions/TerminalZoomAction.kt
Normal file
23
src/main/kotlin/app/termora/actions/TerminalZoomAction.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Database
|
||||||
|
import app.termora.TerminalPanelFactory
|
||||||
|
|
||||||
|
abstract class TerminalZoomAction : AnAction() {
|
||||||
|
protected val fontSize get() = Database.getDatabase().terminal.fontSize
|
||||||
|
|
||||||
|
abstract fun zoom(): Boolean
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
|
if (zoom()) {
|
||||||
|
ApplicationScope.windowScopes().forEach {
|
||||||
|
TerminalPanelFactory.getInstance(it)
|
||||||
|
.fireResize()
|
||||||
|
}
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/kotlin/app/termora/actions/TerminalZoomInAction.kt
Normal file
20
src/main/kotlin/app/termora/actions/TerminalZoomInAction.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
|
import app.termora.I18n
|
||||||
|
|
||||||
|
class TerminalZoomInAction : TerminalZoomAction() {
|
||||||
|
companion object {
|
||||||
|
const val ZOOM_IN = "TerminalZoomInAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(ACTION_COMMAND_KEY, ZOOM_IN)
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-in-terminal"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun zoom(): Boolean {
|
||||||
|
Database.getDatabase().terminal.fontSize += 2
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/kotlin/app/termora/actions/TerminalZoomOutAction.kt
Normal file
22
src/main/kotlin/app/termora/actions/TerminalZoomOutAction.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
|
import app.termora.I18n
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class TerminalZoomOutAction : TerminalZoomAction() {
|
||||||
|
companion object {
|
||||||
|
const val ZOOM_OUT = "TerminalZoomOutAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(ACTION_COMMAND_KEY, ZOOM_OUT)
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-out-terminal"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun zoom(): Boolean {
|
||||||
|
val oldFontSize = fontSize
|
||||||
|
Database.getDatabase().terminal.fontSize = max(fontSize - 2, 9)
|
||||||
|
return oldFontSize != fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.Database
|
||||||
|
import app.termora.I18n
|
||||||
|
|
||||||
|
class TerminalZoomResetAction : TerminalZoomAction() {
|
||||||
|
companion object {
|
||||||
|
const val ZOOM_RESET = "TerminalZoomResetAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(ACTION_COMMAND_KEY, ZOOM_RESET)
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.zoom-reset-terminal"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val defaultFontSize = 14
|
||||||
|
|
||||||
|
override fun zoom(): Boolean {
|
||||||
|
if (fontSize == defaultFontSize) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Database.getDatabase().terminal.fontSize = defaultFontSize
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package app.termora.findeverywhere
|
package app.termora.findeverywhere
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.DialogWrapper
|
||||||
|
import app.termora.DynamicColor
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.macro.MacroFindEverywhereProvider
|
import app.termora.macro.MacroFindEverywhereProvider
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
import com.jetbrains.JBR
|
import com.jetbrains.JBR
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
@@ -20,8 +23,6 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
private val model = DefaultListModel<FindEverywhereResult>()
|
private val model = DefaultListModel<FindEverywhereResult>()
|
||||||
private val resultList = FindEverywhereXList(model)
|
private val resultList = FindEverywhereXList(model)
|
||||||
private val centerPanel = JPanel(BorderLayout())
|
private val centerPanel = JPanel(BorderLayout())
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val providers = mutableListOf<FindEverywhereProvider>(
|
private val providers = mutableListOf<FindEverywhereProvider>(
|
||||||
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
|
||||||
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
|
||||||
@@ -29,15 +30,6 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun registerProvider(provider: FindEverywhereProvider) {
|
|
||||||
providers.add(provider)
|
|
||||||
providers.sortBy { it.order() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unregisterProvider(provider: FindEverywhereProvider) {
|
|
||||||
providers.remove(provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -154,7 +146,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
action =
|
action =
|
||||||
if (resultList.selectedIndex + 1 == resultList.elementCount) {
|
if (resultList.selectedIndex + 1 == resultList.elementCount) {
|
||||||
object : AnAction() {
|
object : AnAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
resultList.selectedIndex = 1
|
resultList.selectedIndex = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,12 +167,12 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
|
|
||||||
resultList.actionMap.put("action", object : AnAction() {
|
resultList.actionMap.put("action", object : AnAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (resultList.selectedIndex < 0) {
|
if (resultList.selectedIndex < 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val event = ActionEvent(e.source, ActionEvent.ACTION_PERFORMED, String())
|
val event = ActionEvent(evt.source, ActionEvent.ACTION_PERFORMED, String())
|
||||||
|
|
||||||
// fire
|
// fire
|
||||||
SwingUtilities.invokeLater { model.get(resultList.selectedIndex).actionPerformed(event) }
|
SwingUtilities.invokeLater { model.get(resultList.selectedIndex).actionPerformed(event) }
|
||||||
@@ -203,22 +195,15 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowClosed(e: WindowEvent) {
|
|
||||||
ActionManager.getInstance()
|
|
||||||
.getAction(Actions.FIND_EVERYWHERE)
|
|
||||||
.isEnabled = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun windowOpened(e: WindowEvent) {
|
fun registerProvider(provider: FindEverywhereProvider) {
|
||||||
ActionManager.getInstance()
|
providers.add(provider)
|
||||||
.getAction(Actions.FIND_EVERYWHERE)
|
providers.sortBy { it.order() }
|
||||||
.isEnabled = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
fun unregisterProvider(provider: FindEverywhereProvider) {
|
||||||
|
providers.remove(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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(NAME, I18n.getString("termora.find-everywhere"))
|
||||||
|
putValue(ACTION_COMMAND_KEY, FIND_EVERYWHERE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
|
||||||
|
val scope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
if (scope.getBoolean("FindEverywhereShown", false)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val source = evt.source
|
||||||
|
if (source !is Component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val owner = evt.window
|
||||||
|
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||||
|
|
||||||
|
if (owner != focusedWindow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = FindEverywhere(owner)
|
||||||
|
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
|
||||||
|
dialog.registerProvider(provider)
|
||||||
|
}
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
scope.putBoolean("FindEverywhereShown", false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialog.isVisible = true
|
||||||
|
|
||||||
|
scope.putBoolean("FindEverywhereShown", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
package app.termora.findeverywhere
|
package app.termora.findeverywhere
|
||||||
|
|
||||||
|
import app.termora.Scope
|
||||||
|
|
||||||
interface FindEverywhereProvider {
|
interface FindEverywhereProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
|
||||||
|
var list = scope.getAnyOrNull("FindEverywhereProviders")
|
||||||
|
if (list == null) {
|
||||||
|
list = mutableListOf<FindEverywhereProvider>()
|
||||||
|
scope.putAny("FindEverywhereProviders", list)
|
||||||
|
}
|
||||||
|
return list as MutableList<FindEverywhereProvider>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索
|
* 搜索
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package app.termora.findeverywhere
|
|||||||
|
|
||||||
import app.termora.Actions
|
import app.termora.Actions
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
|
||||||
class QuickActionsFindEverywhereProvider : FindEverywhereProvider {
|
class QuickActionsFindEverywhereProvider : FindEverywhereProvider {
|
||||||
private val actions = listOf(Actions.KEY_MANAGER, Actions.KEYWORD_HIGHLIGHT_EVERYWHERE, Actions.MULTIPLE)
|
private val actions = listOf(
|
||||||
|
Actions.KEY_MANAGER,
|
||||||
|
Actions.KEYWORD_HIGHLIGHT,
|
||||||
|
Actions.MULTIPLE,
|
||||||
|
)
|
||||||
|
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||||
val actionManager = ActionManager.getInstance()
|
val actionManager = ActionManager.getInstance()
|
||||||
return actions
|
return actions
|
||||||
|
|||||||
@@ -1,54 +1,30 @@
|
|||||||
package app.termora.findeverywhere
|
package app.termora.findeverywhere
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.Actions
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.actions.NewHostAction
|
||||||
|
import app.termora.actions.OpenLocalTerminalAction
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||||
|
private val actionManager get() = ActionManager.getInstance()
|
||||||
|
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||||
val list = mutableListOf<FindEverywhereResult>()
|
val list = mutableListOf<FindEverywhereResult>()
|
||||||
ActionManager.getInstance().getAction(Actions.ADD_HOST)?.let {
|
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
|
||||||
list.add(CreateHostFindEverywhereResult())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local terminal
|
// Local terminal
|
||||||
list.add(ActionFindEverywhereResult(object : AnAction(
|
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
|
||||||
I18n.getString("termora.find-everywhere.quick-command.local-terminal"),
|
list.add(ActionFindEverywhereResult(it))
|
||||||
Icons.terminal
|
|
||||||
) {
|
|
||||||
override fun actionPerformed(evt: ActionEvent) {
|
|
||||||
ActionManager.getInstance().getAction(Actions.OPEN_HOST)
|
|
||||||
?.actionPerformed(
|
|
||||||
OpenHostActionEvent(
|
|
||||||
this, Host(
|
|
||||||
name = name,
|
|
||||||
protocol = Protocol.Local
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
|
|
||||||
// SFTP
|
// SFTP
|
||||||
list.add(ActionFindEverywhereResult(object : AnAction("SFTP", Icons.fileTransfer) {
|
actionManager.getAction(Actions.SFTP)?.let {
|
||||||
override fun actionPerformed(evt: ActionEvent) {
|
list.add(ActionFindEverywhereResult(it))
|
||||||
val terminalTabbedManager = Application.getService(TerminalTabbedManager::class)
|
|
||||||
val tabs = terminalTabbedManager.getTerminalTabs()
|
|
||||||
for (i in tabs.indices) {
|
|
||||||
val tab = tabs[i]
|
|
||||||
if (tab is SFTPTerminalTab) {
|
|
||||||
terminalTabbedManager.setSelectedTerminalTab(tab)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 创建一个新的
|
|
||||||
terminalTabbedManager.addTerminalTab(SFTPTerminalTab())
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
@@ -63,7 +39,7 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class CreateHostFindEverywhereResult : ActionFindEverywhereResult(
|
private class CreateHostFindEverywhereResult : ActionFindEverywhereResult(
|
||||||
ActionManager.getInstance().getAction(Actions.ADD_HOST)
|
ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
|
||||||
) {
|
) {
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.highlight
|
package app.termora.highlight
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
import app.termora.DialogWrapper
|
import app.termora.DialogWrapper
|
||||||
import app.termora.TerminalFactory
|
import app.termora.TerminalFactory
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
@@ -30,7 +31,8 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val panel = JPanel(GridLayout(2, 8, 4, 4))
|
val panel = JPanel(GridLayout(2, 8, 4, 4))
|
||||||
val colorPalette = TerminalFactory.instance.createTerminal().getTerminalModel().getColorPalette()
|
val colorPalette = TerminalFactory.getInstance(ApplicationScope.forWindowScope(this))
|
||||||
|
.createTerminal().getTerminalModel().getColorPalette()
|
||||||
for (i in 1..16) {
|
for (i in 1..16) {
|
||||||
val c = JPanel()
|
val c = JPanel()
|
||||||
c.preferredSize = Dimension(24, 24)
|
c.preferredSize = Dimension(24, 24)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package app.termora.highlight
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.actions.AnAction
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
|
||||||
|
class KeywordHighlightAction : AnAction(
|
||||||
|
I18n.getString("termora.highlight"),
|
||||||
|
Icons.edit
|
||||||
|
) {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val owner = evt.window
|
||||||
|
val dialog = KeywordHighlightDialog(owner)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,11 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
private val model = KeywordHighlightTableModel()
|
private val model = KeywordHighlightTableModel()
|
||||||
private val table = FlatTable()
|
private val table = FlatTable()
|
||||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.instance }
|
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||||
private val colorPalette by lazy { TerminalFactory.instance.createTerminal().getTerminalModel().getColorPalette() }
|
private val colorPalette by lazy {
|
||||||
|
TerminalFactory.getInstance(ApplicationScope.forWindowScope(this)).createTerminal().getTerminalModel()
|
||||||
|
.getColorPalette()
|
||||||
|
}
|
||||||
|
|
||||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user