mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c26e3d08a | ||
|
|
9b84fb4ec8 | ||
|
|
d8ec7b6d4a | ||
|
|
769c0d990b | ||
|
|
3f1ae38b61 | ||
|
|
e10fce21a2 | ||
|
|
a00557bb9d | ||
|
|
e478535ae5 | ||
|
|
7756758738 | ||
|
|
e0ea42faee | ||
|
|
e72c6b77b5 | ||
|
|
bcd3aacd6f | ||
|
|
570b0e08ad | ||
|
|
d703850e87 | ||
|
|
4bb1a411e8 | ||
|
|
9884ed19fa | ||
|
|
1ffaed3f36 | ||
|
|
4cb42953ad | ||
|
|
0248992dc3 | ||
|
|
9bab9db875 | ||
|
|
b283a3ea38 | ||
|
|
98ac2928b4 | ||
|
|
a0a6f43c10 | ||
|
|
0c158acbe0 | ||
|
|
9a97b3a304 | ||
|
|
aef44bd0da | ||
|
|
75c65d9ba8 | ||
|
|
93755db77f | ||
|
|
79d0a9a348 | ||
|
|
422e9aac84 | ||
|
|
9915c373b7 | ||
|
|
eba85e6348 | ||
|
|
483a7772f4 | ||
|
|
dcc96358f6 | ||
|
|
b5c30d505b | ||
|
|
1f3ef5f3f0 | ||
|
|
d388bcfc92 | ||
|
|
562c1f98fe | ||
|
|
f3c5009a45 | ||
|
|
09a1d9f51e | ||
|
|
84b48278ad | ||
|
|
ef9caf2578 | ||
|
|
b85bdf840e | ||
|
|
a2d7f3b5bb | ||
|
|
02a96d73c8 | ||
|
|
9fb12c7a71 | ||
|
|
145d8fc802 | ||
|
|
72c9dba806 | ||
|
|
de20bd654c | ||
|
|
35b3a10746 | ||
|
|
05fe6a0eb1 | ||
|
|
0552917c26 | ||
|
|
51c355c113 | ||
|
|
034ee3791d | ||
|
|
adabaf8f2d | ||
|
|
1f392c52a1 | ||
|
|
28fe4c725f | ||
|
|
18fe92cb11 | ||
|
|
c49acf7b51 | ||
|
|
7df317a1b9 | ||
|
|
219e5420f5 | ||
|
|
aefb7c3014 | ||
|
|
f0c7f06ff5 | ||
|
|
604e07b43a | ||
|
|
0000e4610a | ||
|
|
510324d7c4 | ||
|
|
33a359fcbf | ||
|
|
0b84d3271c | ||
|
|
57547c95cb | ||
|
|
503cfa9a4e | ||
|
|
af1f979e31 | ||
|
|
3cd9f92ea9 | ||
|
|
b332bada95 | ||
|
|
63a12c2ec8 | ||
|
|
743f242805 | ||
|
|
5bead0b27d | ||
|
|
73e3c7016b | ||
|
|
3829dcd0f9 | ||
|
|
b2047044fe | ||
|
|
47d1a13189 | ||
|
|
309909cbd7 | ||
|
|
b5cebb4cea | ||
|
|
b6dd2693cd | ||
|
|
5fdfe98f26 | ||
|
|
0c768aa1ca | ||
|
|
d493e6dc9e | ||
|
|
7e0c7d8891 | ||
|
|
3510c6600d | ||
|
|
32d91150bd | ||
|
|
bbf2d50e3f | ||
|
|
39725f9828 | ||
|
|
1e8c617a85 | ||
|
|
7f8573ec4c | ||
|
|
d8e629917e | ||
|
|
bdc0a15439 | ||
|
|
a25b97614f | ||
|
|
4e12c32566 | ||
|
|
ea9c0f1225 | ||
|
|
ff865f13a2 | ||
|
|
9875200912 | ||
|
|
9f218d004e | ||
|
|
ab727f66f4 | ||
|
|
efbc0302e4 | ||
|
|
ab2367d670 | ||
|
|
045e4f81d6 | ||
|
|
160cfee947 | ||
|
|
0e40b5ecce | ||
|
|
fcaddcee80 | ||
|
|
8d6295fd3b | ||
|
|
d0d51b3e6f | ||
|
|
b8d612f1d5 | ||
|
|
f7c49cde0c | ||
|
|
189f8fb3ba | ||
|
|
2a64bd28a8 | ||
|
|
8a733379a3 | ||
|
|
e5f854dfcd | ||
|
|
4e690bafed | ||
|
|
28b511e179 | ||
|
|
f010a13abd | ||
|
|
4d80ffafdd | ||
|
|
9aecd4d54b | ||
|
|
65091823eb | ||
|
|
d17218bfbd | ||
|
|
724c5d2632 | ||
|
|
6806c26028 | ||
|
|
dcd89174c9 | ||
|
|
9a8707b8cb | ||
|
|
28f1d05f06 | ||
|
|
54b044584e | ||
|
|
ed39449a20 | ||
|
|
2ff3f3a352 | ||
|
|
91e2e964a5 | ||
|
|
ca6cc68fed | ||
|
|
0962de7735 | ||
|
|
062b957fdb | ||
|
|
4efe4e5663 | ||
|
|
25eb6966c4 | ||
|
|
7843460020 | ||
|
|
1cbc6ba4a9 | ||
|
|
a43407bee8 | ||
|
|
05c4ec9af2 | ||
|
|
9236064293 | ||
|
|
e1955a371e |
47
.github/workflows/linux-aarch64.yml
vendored
Normal file
47
.github/workflows/linux-aarch64.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Linux aarch64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.tar.gz
|
||||||
|
|
||||||
|
# appimagetool
|
||||||
|
- run: sudo apt install libfuse2
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.6'
|
||||||
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-aarch64
|
||||||
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
20
.github/workflows/linux-x86-64.yml
vendored
20
.github/workflows/linux-x86-64.yml
vendored
@@ -4,14 +4,17 @@ on: [ push, pull_request ]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b825.69.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.tar.gz
|
||||||
|
|
||||||
|
# appimagetool
|
||||||
|
- run: sudo apt install libfuse2
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -22,6 +25,15 @@ jobs:
|
|||||||
java-version: '21.0.6'
|
java-version: '21.0.6'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
@@ -30,4 +42,6 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-linux-x86-64
|
name: termora-linux-x86-64
|
||||||
path: build/distributions/*.tar.gz
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
|
|||||||
55
.github/workflows/osx-aarch64.yml
vendored
55
.github/workflows/osx-aarch64.yml
vendored
@@ -10,9 +10,41 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
- name: Setup the Notary information
|
||||||
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -23,13 +55,30 @@ jobs:
|
|||||||
java-version: '21.0.6'
|
java-version: '21.0.6'
|
||||||
architecture: aarch64
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- name: Dist
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
# 只有发布版本时才需要公证
|
||||||
|
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||||
|
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-osx-aarch64
|
name: termora-osx-aarch64
|
||||||
path: build/distributions/*.dmg
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
build/distributions/*.dmg
|
||||||
|
|||||||
58
.github/workflows/osx-x86-64.yml
vendored
58
.github/workflows/osx-x86-64.yml
vendored
@@ -10,8 +10,41 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
- name: Setup the Notary information
|
||||||
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -23,12 +56,31 @@ jobs:
|
|||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- name: Dist
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
# 只有发布版本时才需要公证
|
||||||
|
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||||
|
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-osx-x86-64
|
name: termora-osx-x86-64
|
||||||
path: build/distributions/*.dmg
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
build/distributions/*.dmg
|
||||||
|
|||||||
27
.github/workflows/windows-x86-64.yml
vendored
27
.github/workflows/windows-x86-64.yml
vendored
@@ -10,15 +10,34 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install zip
|
||||||
|
run: |
|
||||||
|
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
||||||
|
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
||||||
|
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
||||||
|
|
||||||
|
- name: Install 7z
|
||||||
|
uses: milliewalky/setup-7-zip@v2
|
||||||
|
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
uses: actions/setup-java@v4
|
run: |
|
||||||
|
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-windows-x64-b895.91.zip
|
||||||
|
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
|
||||||
|
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.6-windows-x64-b895.91" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'jetbrains'
|
path: |
|
||||||
java-version: '21'
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- run: |
|
||||||
.\gradlew.bat dist --no-daemon
|
.\gradlew.bat dist --no-daemon
|
||||||
|
.\gradlew.bat --stop
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -26,4 +45,4 @@ jobs:
|
|||||||
name: termora-windows-x86-64
|
name: termora-windows-x86-64
|
||||||
path: |
|
path: |
|
||||||
build/distributions/*.zip
|
build/distributions/*.zip
|
||||||
build/distributions/*.msi
|
build/distributions/*.exe
|
||||||
|
|||||||
14
.github/workflows/winget.yml
vendored
Normal file
14
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: Publish to WinGet
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ released ]
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
|
if: github.repository == 'TermoraDev/termora'
|
||||||
|
with:
|
||||||
|
identifier: TermoraDev.Termora
|
||||||
|
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
@@ -15,12 +15,13 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- SSH and local terminal support
|
- SSH and local terminal support
|
||||||
- [SFTP](./docs/sftp.png?raw=1) file transfer support
|
- Serial port protocol support
|
||||||
|
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
||||||
- Compatible with Windows, macOS, and Linux
|
- Compatible with Windows, macOS, and Linux
|
||||||
- Zmodem protocol support
|
- Zmodem protocol support
|
||||||
- SSH port forwarding & Jump hosts
|
- SSH port forwarding & Jump hosts
|
||||||
- Terminal log
|
- Terminal log
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com)
|
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
- Macro support (record and replay scripts)
|
- Macro support (record and replay scripts)
|
||||||
- Keyword highlighting
|
- Keyword highlighting
|
||||||
- Key management
|
- Key management
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
|
|
||||||
- [Latest release](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`
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,13 @@
|
|||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 支持 SSH 和本地终端
|
- 支持 SSH 和本地终端
|
||||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
|
- 支持串口协议
|
||||||
|
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
|
||||||
- 支持 Windows、macOS、Linux 平台
|
- 支持 Windows、macOS、Linux 平台
|
||||||
- 支持 Zmodem 协议
|
- 支持 Zmodem 协议
|
||||||
- 支持 SSH 端口转发和跳板机
|
- 支持 SSH 端口转发和跳板机
|
||||||
- 终端日志记录
|
- 终端日志记录
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com)
|
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
- 支持宏(录制脚本并回放)
|
- 支持宏(录制脚本并回放)
|
||||||
- 支持关键词高亮
|
- 支持关键词高亮
|
||||||
- 支持密钥管理器
|
- 支持密钥管理器
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
|
|
||||||
- [Latest release](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`
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|||||||
22
THIRDPARTY
22
THIRDPARTY
@@ -14,7 +14,7 @@ commonmark 0.24.0
|
|||||||
BSD 2-Clause "Simplified" License
|
BSD 2-Clause "Simplified" License
|
||||||
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||||
|
|
||||||
commons-codec 1.17.1
|
commons-codec 1.18.0
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||||
|
|
||||||
@@ -34,10 +34,18 @@ commons-net 3.11.1
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-text 1.12.0
|
commons-text 1.13.0
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
commons-csv 1.13.0
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
ini4j 0.5.5-2
|
||||||
|
Apache License 2.0
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
eddsa 0.3.0
|
eddsa 0.3.0
|
||||||
Creative Commons Zero v1.0 Universal
|
Creative Commons Zero v1.0 Universal
|
||||||
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||||
@@ -110,7 +118,7 @@ kotlin-logging 1.7.9
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||||
|
|
||||||
kotlin-stdlib 2.1.0
|
kotlin-stdlib 2.1.10
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
@@ -126,6 +134,10 @@ kotlin-stdlib-jdk8 1.9.10
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
|
restart4j 0.0.1
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/hstyi/restart4j/blob/main/LICENSE
|
||||||
|
|
||||||
kotlinx-coroutines-core-jvm 1.10.1
|
kotlinx-coroutines-core-jvm 1.10.1
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@@ -134,11 +146,11 @@ kotlinx-coroutines-swing 1.10.1
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
kotlinx-serialization-core-jvm 1.7.3
|
kotlinx-serialization-core-jvm 1.8.0
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||||
|
|
||||||
kotlinx-serialization-json-jvm 1.7.3
|
kotlinx-serialization-json-jvm 1.8.0
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
|||||||
461
build.gradle.kts
461
build.gradle.kts
@@ -3,11 +3,16 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
|||||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||||
|
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
|
idea
|
||||||
application
|
application
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinx.serialization)
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
@@ -15,7 +20,7 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.6"
|
version = "1.0.11"
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
@@ -38,7 +43,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// 由于签名和公证,macOS 不携带 natives
|
// 由于签名和公证,macOS 不携带 natives
|
||||||
val useNoNativesFlatLaf = os.isMacOsX && macOSNotary && System.getenv("ENABLE_BUILD").toBoolean()
|
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(libs.hutool)
|
testImplementation(libs.hutool)
|
||||||
@@ -58,6 +63,7 @@ dependencies {
|
|||||||
implementation(libs.commons.codec)
|
implementation(libs.commons.codec)
|
||||||
implementation(libs.commons.io)
|
implementation(libs.commons.io)
|
||||||
implementation(libs.commons.lang3)
|
implementation(libs.commons.lang3)
|
||||||
|
implementation(libs.commons.csv)
|
||||||
implementation(libs.commons.net)
|
implementation(libs.commons.net)
|
||||||
implementation(libs.commons.text)
|
implementation(libs.commons.text)
|
||||||
implementation(libs.commons.compress)
|
implementation(libs.commons.compress)
|
||||||
@@ -97,7 +103,7 @@ dependencies {
|
|||||||
implementation(libs.sshd.core)
|
implementation(libs.sshd.core)
|
||||||
implementation(libs.commonmark)
|
implementation(libs.commonmark)
|
||||||
implementation(libs.jgit)
|
implementation(libs.jgit)
|
||||||
implementation(libs.jgit.sshd)
|
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.jnafilechooser)
|
implementation(libs.jnafilechooser)
|
||||||
implementation(libs.xodus.vfs)
|
implementation(libs.xodus.vfs)
|
||||||
implementation(libs.xodus.openAPI)
|
implementation(libs.xodus.openAPI)
|
||||||
@@ -106,21 +112,24 @@ dependencies {
|
|||||||
implementation(libs.colorpicker)
|
implementation(libs.colorpicker)
|
||||||
implementation(libs.mixpanel)
|
implementation(libs.mixpanel)
|
||||||
implementation(libs.jSerialComm)
|
implementation(libs.jSerialComm)
|
||||||
|
implementation(libs.ini4j)
|
||||||
|
implementation(libs.restart4j)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
val args = mutableListOf(
|
val args = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
|
||||||
"-Xmx2g",
|
"-Xmx2g",
|
||||||
"-XX:+UseZGC",
|
"-XX:+UseZGC",
|
||||||
"-XX:+ZUncommit",
|
"-XX:+ZUncommit",
|
||||||
"-XX:+ZGenerational",
|
"-XX:+ZGenerational",
|
||||||
"-XX:ZUncommitDelay=60",
|
"-XX:ZUncommitDelay=60",
|
||||||
"-XX:SoftMaxHeapSize=64m"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
args.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
// macOS NSWindow
|
||||||
|
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
args.add("-Dsun.java2d.metal=true")
|
args.add("-Dsun.java2d.metal=true")
|
||||||
args.add("-Dapple.awt.application.appearance=system")
|
args.add("-Dapple.awt.application.appearance=system")
|
||||||
}
|
}
|
||||||
@@ -142,16 +151,17 @@ tasks.test {
|
|||||||
tasks.register<Copy>("copy-dependencies") {
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
val dir = layout.buildDirectory.dir("libs")
|
val dir = layout.buildDirectory.dir("libs")
|
||||||
from(configurations.runtimeClasspath).into(dir)
|
from(configurations.runtimeClasspath).into(dir)
|
||||||
|
val jna = libs.jna.asProvider().get()
|
||||||
|
val pty4j = libs.pty4j.get()
|
||||||
|
val jSerialComm = libs.jSerialComm.get()
|
||||||
|
val restart4j = libs.restart4j.get()
|
||||||
|
|
||||||
// 对 JNA 和 PTY4J 的本地库提取
|
// 对 JNA 和 PTY4J 的本地库提取
|
||||||
// 提取出来是为了单独签名,不然无法通过公证
|
// 提取出来是为了单独签名,不然无法通过公证
|
||||||
if (os.isMacOsX && macOSSign) {
|
if (os.isMacOsX && macOSSign) {
|
||||||
doLast {
|
doLast {
|
||||||
val jna = libs.jna.asProvider().get()
|
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||||
val dylib = dir.get().dir("dylib").asFile
|
val dylib = dir.get().dir("dylib").asFile
|
||||||
val pty4j = libs.pty4j.get()
|
|
||||||
val jSerialComm = libs.jSerialComm.get()
|
|
||||||
|
|
||||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||||
val targetDir = File(dylib, jna.name)
|
val targetDir = File(dylib, jna.name)
|
||||||
@@ -177,7 +187,6 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
// 删除所有二进制类库
|
// 删除所有二进制类库
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
|
||||||
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
||||||
FileUtils.forceMkdir(targetDir)
|
FileUtils.forceMkdir(targetDir)
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
@@ -191,6 +200,24 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||||
|
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = FileUtils.getFile(dylib, restart4j.name)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 删除所有二进制类库
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
|
||||||
|
// 设置可执行权限
|
||||||
|
for (e in FileUtils.listFiles(
|
||||||
|
targetDir,
|
||||||
|
FileFilterUtils.trueFileFilter(),
|
||||||
|
FileFilterUtils.falseFileFilter()
|
||||||
|
)) {
|
||||||
|
e.setExecutable(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +230,73 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (os.isLinux || os.isWindows) { // 缩减安装包
|
||||||
|
doLast {
|
||||||
|
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||||
|
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
|
||||||
|
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/freebsd-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
||||||
|
if (os.isWindows) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-aarch64/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86/*") }
|
||||||
|
}
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
|
||||||
|
}
|
||||||
|
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*darwin*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*freebsd*") }
|
||||||
|
if (os.isWindows) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*linux*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86-64*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/aarch64/*") }
|
||||||
|
}
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win*") }
|
||||||
|
}
|
||||||
|
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||||
|
if (os.isWindows) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||||
|
}
|
||||||
|
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
|
||||||
|
if (os.isWindows) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "win32/x86_64/*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "win32/aarch64/*") }
|
||||||
|
}
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "linux/x86_64/*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,22 +338,28 @@ tasks.register<Exec>("jpackage") {
|
|||||||
"-XX:+ZUncommit",
|
"-XX:+ZUncommit",
|
||||||
"-XX:+ZGenerational",
|
"-XX:+ZGenerational",
|
||||||
"-XX:ZUncommitDelay=60",
|
"-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",
|
||||||
"-Dapp-version=${project.version}",
|
"-Dapp-version=${project.version}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
options.add("-Dsun.java2d.metal=true")
|
// NSWindow
|
||||||
|
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
options.add("-Dsun.java2d.opengl=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage", "--verbose")
|
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
||||||
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
||||||
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
||||||
arguments.addAll(listOf("--app-version", "${project.version}"))
|
arguments.addAll(listOf("--app-version", "${project.version}"))
|
||||||
@@ -271,7 +371,17 @@ tasks.register<Exec>("jpackage") {
|
|||||||
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("--vendor", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
|
||||||
|
if (os.isWindows) {
|
||||||
|
arguments.addAll(
|
||||||
|
listOf(
|
||||||
|
"--description",
|
||||||
|
"${project.name.uppercaseFirstChar()}: A terminal emulator and SSH client"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -289,6 +399,10 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.png"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
arguments.add("--type")
|
arguments.add("--type")
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -313,17 +427,8 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
tasks.register("dist") {
|
tasks.register("dist") {
|
||||||
doLast {
|
doLast {
|
||||||
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
|
|
||||||
@Suppress("UnstableApiUsage")
|
|
||||||
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
|
|
||||||
throw GradleException("JVM: $vendor is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
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") }
|
||||||
@@ -343,75 +448,8 @@ tasks.register("dist") {
|
|||||||
// 打包
|
// 打包
|
||||||
exec { commandLine(gradlew, "jpackage") }
|
exec { commandLine(gradlew, "jpackage") }
|
||||||
|
|
||||||
// pack
|
// 根据不同的系统构建不同的二进制包
|
||||||
if (os.isWindows) { // zip and msi
|
pack()
|
||||||
// zip
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"tar", "-vacf",
|
|
||||||
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
|
||||||
project.name.uppercaseFirstChar()
|
|
||||||
)
|
|
||||||
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
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"tar", "-czvf",
|
|
||||||
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
|
||||||
project.name.uppercaseFirstChar()
|
|
||||||
)
|
|
||||||
workingDir = distributionDir.asFile
|
|
||||||
}
|
|
||||||
} else if (os.isMacOsX) { // rename
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"mv",
|
|
||||||
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
|
||||||
macOSFinalFilePath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,6 +487,198 @@ tasks.register("check-license") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建包
|
||||||
|
*/
|
||||||
|
fun pack() {
|
||||||
|
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||||
|
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||||
|
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||||
|
val projectName = project.name.uppercaseFirstChar()
|
||||||
|
|
||||||
|
if (os.isWindows) {
|
||||||
|
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
|
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
|
} else {
|
||||||
|
throw GradleException("${os.name} is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 zip、7z、msi
|
||||||
|
*/
|
||||||
|
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
|
// zip
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"tar", "-vacf",
|
||||||
|
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||||
|
projectName
|
||||||
|
)
|
||||||
|
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// exe
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"iscc",
|
||||||
|
"/DMyAppId=${projectName}",
|
||||||
|
"/DMyAppName=${projectName}",
|
||||||
|
"/DMyAppVersion=${project.version}",
|
||||||
|
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
||||||
|
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
||||||
|
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
|
||||||
|
"/F${finalFilenameWithoutExtension}",
|
||||||
|
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// msi
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"cmd", "/c", "move",
|
||||||
|
"${projectName}-${project.version}.msi",
|
||||||
|
"${finalFilenameWithoutExtension}.msi"
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对于 macOS 先对 jpackage 构建的 dmg 重命名 -> 签名 -> 公证,另外还会创建一个 zip 包
|
||||||
|
*/
|
||||||
|
fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
|
val dmgFile = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile
|
||||||
|
val zipFile = distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile
|
||||||
|
|
||||||
|
// rename
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("mv", distributionDir.file("${projectName}-${project.version}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
// sign dmg
|
||||||
|
if (macOSSign) signMacOSLocalFile(dmgFile)
|
||||||
|
|
||||||
|
// 找到 .app
|
||||||
|
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||||
|
val appFile = imageFile.listFiles()?.firstOrNull()?.listFiles()?.firstOrNull()
|
||||||
|
?: throw FileNotFoundException("${projectName}.app")
|
||||||
|
|
||||||
|
// zip
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
// sign zip
|
||||||
|
if (macOSSign) signMacOSLocalFile(zipFile)
|
||||||
|
|
||||||
|
// 公证
|
||||||
|
if (macOSNotary) {
|
||||||
|
val pool = Executors.newCachedThreadPool()
|
||||||
|
val jobs = mutableListOf<Future<*>>()
|
||||||
|
|
||||||
|
// zip
|
||||||
|
pool.submit {
|
||||||
|
// 对 zip 公证
|
||||||
|
notaryMacOSLocalFile(zipFile)
|
||||||
|
// 对 .app 盖章
|
||||||
|
stapleMacOSLocalFile(appFile)
|
||||||
|
// 删除旧的 zip ,旧的 zip 仅仅是为了公证
|
||||||
|
FileUtils.deleteQuietly(zipFile)
|
||||||
|
// 再对盖完章的 app 打成 zip 包
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 再对 zip 签名
|
||||||
|
signMacOSLocalFile(zipFile)
|
||||||
|
}.apply { jobs.add(this) }
|
||||||
|
|
||||||
|
// dmg
|
||||||
|
pool.submit {
|
||||||
|
// 公证
|
||||||
|
notaryMacOSLocalFile(dmgFile)
|
||||||
|
// 盖章
|
||||||
|
stapleMacOSLocalFile(dmgFile)
|
||||||
|
}.apply { jobs.add(this) }
|
||||||
|
|
||||||
|
// join ...
|
||||||
|
jobs.forEach { it.get() }
|
||||||
|
|
||||||
|
// shutdown
|
||||||
|
pool.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 tar.gz 和 AppImage
|
||||||
|
*/
|
||||||
|
fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
|
// tar.gz
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"tar", "-czvf",
|
||||||
|
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||||
|
projectName
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// AppImage
|
||||||
|
// Download AppImageKit
|
||||||
|
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
|
||||||
|
if (!appimagetool.exists()) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"wget",
|
||||||
|
"-O", appimagetool.absolutePath,
|
||||||
|
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppImageKit chmod
|
||||||
|
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Desktop file
|
||||||
|
val termoraName = project.name.uppercaseFirstChar()
|
||||||
|
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||||
|
desktopFile.writeText(
|
||||||
|
"""[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=${termoraName}
|
||||||
|
Comment=Terminal emulator and SSH client
|
||||||
|
Icon=/lib/${termoraName}
|
||||||
|
Categories=Development;
|
||||||
|
Terminal=false
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppRun file
|
||||||
|
val appRun = File(desktopFile.parentFile, "AppRun")
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append("#!/bin/sh").appendLine()
|
||||||
|
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
|
||||||
|
sb.append("HERE=\${SELF%/*}").appendLine()
|
||||||
|
sb.append("export LinuxAppImage=true").appendLine()
|
||||||
|
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||||
|
appRun.writeText(sb.toString())
|
||||||
|
appRun.setExecutable(true)
|
||||||
|
|
||||||
|
// AppImage
|
||||||
|
exec {
|
||||||
|
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* macOS 对本地文件进行签名
|
* macOS 对本地文件进行签名
|
||||||
*/
|
*/
|
||||||
@@ -468,11 +698,50 @@ fun signMacOSLocalFile(file: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* macOS 对本地文件进行公证
|
||||||
|
*/
|
||||||
|
fun notaryMacOSLocalFile(file: File) {
|
||||||
|
if (os.isMacOsX && macOSNotary) {
|
||||||
|
if (file.exists()) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun", "notarytool",
|
||||||
|
"submit", file,
|
||||||
|
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||||
|
"--wait",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盖章
|
||||||
|
*/
|
||||||
|
fun stapleMacOSLocalFile(file: File) {
|
||||||
|
if (os.isMacOsX && macOSNotary) {
|
||||||
|
if (file.exists()) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
"stapler", "staple", file,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain {
|
jvmToolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
@Suppress("UnstableApiUsage")
|
}
|
||||||
vendor = JvmVendorSpec.JETBRAINS
|
}
|
||||||
|
|
||||||
|
idea {
|
||||||
|
module {
|
||||||
|
isDownloadJavadoc = true
|
||||||
|
isDownloadSources = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
docs/sftp-command.png
Normal file
BIN
docs/sftp-command.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -1,16 +1,17 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.0"
|
kotlin = "2.1.10"
|
||||||
slf4j = "2.0.16"
|
slf4j = "2.0.16"
|
||||||
pty4j = "0.13.2"
|
pty4j = "0.13.2"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.1"
|
||||||
flatlaf = "3.5.4"
|
flatlaf = "3.5.4"
|
||||||
trove4j = "1.0.20200330"
|
trove4j = "1.0.20200330"
|
||||||
kotlinx-serialization-json = "1.7.3"
|
kotlinx-serialization-json = "1.8.0"
|
||||||
commons-codec = "1.17.1"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.17.0"
|
||||||
|
commons-csv = "1.13.0"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.11.1"
|
||||||
commons-text = "1.12.0"
|
commons-text = "1.13.0"
|
||||||
commons-compress = "1.27.1"
|
commons-compress = "1.27.1"
|
||||||
koin-bom = "4.0.0"
|
koin-bom = "4.0.0"
|
||||||
swingx = "1.6.5-1"
|
swingx = "1.6.5-1"
|
||||||
@@ -41,7 +42,9 @@ 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"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm="2.11.0"
|
jSerialComm = "2.11.0"
|
||||||
|
ini4j = "0.5.5-2"
|
||||||
|
restart4j = "0.0.1"
|
||||||
|
|
||||||
[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" }
|
||||||
@@ -53,9 +56,11 @@ tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "ti
|
|||||||
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
|
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
|
||||||
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
|
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
|
||||||
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
|
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
|
||||||
|
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
|
||||||
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
||||||
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
||||||
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
||||||
|
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
||||||
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||||
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
||||||
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
||||||
@@ -72,6 +77,7 @@ versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "ve
|
|||||||
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
||||||
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
||||||
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
||||||
|
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
||||||
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||||
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||||
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
|
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
|
||||||
|
|||||||
139
src/main/java/app/termora/MyFlatTabbedPaneUI.java
Normal file
139
src/main/java/app/termora/MyFlatTabbedPaneUI.java
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package app.termora;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.ui.FlatTabbedPaneUI;
|
||||||
|
import com.formdev.flatlaf.ui.FlatUIUtils;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
|
||||||
|
import static com.formdev.flatlaf.FlatClientProperties.*;
|
||||||
|
import static com.formdev.flatlaf.util.UIScale.scale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果要升级 FlatLaf 需要检查是否兼容
|
||||||
|
*/
|
||||||
|
public class MyFlatTabbedPaneUI extends FlatTabbedPaneUI {
|
||||||
|
@Override
|
||||||
|
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
|
||||||
|
if (tabPane.getTabCount() <= 0 ||
|
||||||
|
contentSeparatorHeight == 0 ||
|
||||||
|
!clientPropertyBoolean(tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, showContentSeparator))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Insets insets = tabPane.getInsets();
|
||||||
|
Insets tabAreaInsets = getTabAreaInsets(tabPlacement);
|
||||||
|
|
||||||
|
int x = insets.left;
|
||||||
|
int y = insets.top;
|
||||||
|
int w = tabPane.getWidth() - insets.right - insets.left;
|
||||||
|
int h = tabPane.getHeight() - insets.top - insets.bottom;
|
||||||
|
|
||||||
|
// remove tabs from bounds
|
||||||
|
switch (tabPlacement) {
|
||||||
|
case BOTTOM:
|
||||||
|
h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||||
|
h += tabAreaInsets.top;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LEFT:
|
||||||
|
x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
|
||||||
|
x -= tabAreaInsets.right;
|
||||||
|
w -= (x - insets.left);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RIGHT:
|
||||||
|
w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
|
||||||
|
w += tabAreaInsets.left;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOP:
|
||||||
|
default:
|
||||||
|
y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||||
|
y -= tabAreaInsets.bottom;
|
||||||
|
h -= (y - insets.top);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute insets for separator or full border
|
||||||
|
boolean hasFullBorder = clientPropertyBoolean(tabPane, TABBED_PANE_HAS_FULL_BORDER, this.hasFullBorder);
|
||||||
|
int sh = scale(contentSeparatorHeight * 100); // multiply by 100 because rotateInsets() does not use floats
|
||||||
|
Insets ci = new Insets(0, 0, 0, 0);
|
||||||
|
rotateInsets(hasFullBorder ? new Insets(sh, sh, sh, sh) : new Insets(sh, 0, 0, 0), ci, tabPlacement);
|
||||||
|
|
||||||
|
// create path for content separator or full border
|
||||||
|
Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
|
||||||
|
path.append(new Rectangle2D.Float(x, y, w, h), false);
|
||||||
|
path.append(new Rectangle2D.Float(x + (ci.left / 100f), y + (ci.top / 100f),
|
||||||
|
w - (ci.left / 100f) - (ci.right / 100f), h - (ci.top / 100f) - (ci.bottom / 100f)), false);
|
||||||
|
|
||||||
|
// add gap for selected tab to path
|
||||||
|
if (getTabType() == TAB_TYPE_CARD && selectedIndex >= 0) {
|
||||||
|
float csh = scale((float) contentSeparatorHeight);
|
||||||
|
|
||||||
|
Rectangle tabRect = getTabBounds(tabPane, selectedIndex);
|
||||||
|
boolean componentHasFullBorder = false;
|
||||||
|
if (tabPane.getComponentAt(selectedIndex) instanceof JComponent c) {
|
||||||
|
componentHasFullBorder = c.getClientProperty(TABBED_PANE_HAS_FULL_BORDER) == Boolean.TRUE;
|
||||||
|
}
|
||||||
|
Rectangle2D.Float innerTabRect = new Rectangle2D.Float(tabRect.x + csh, tabRect.y + csh,
|
||||||
|
componentHasFullBorder ? 0 : tabRect.width - (csh * 2), tabRect.height - (csh * 2));
|
||||||
|
|
||||||
|
// Ensure that the separator outside the tabViewport is present (doesn't get cutoff by the active tab)
|
||||||
|
// If left unsolved the active tab is "visible" in the separator (the gap) even when outside the viewport
|
||||||
|
if (tabViewport != null)
|
||||||
|
Rectangle2D.intersect(tabViewport.getBounds(), innerTabRect, innerTabRect);
|
||||||
|
|
||||||
|
Rectangle2D.Float gap = null;
|
||||||
|
if (isHorizontalTabPlacement(tabPlacement)) {
|
||||||
|
if (innerTabRect.width > 0) {
|
||||||
|
float y2 = (tabPlacement == TOP) ? y : y + h - csh;
|
||||||
|
gap = new Rectangle2D.Float(innerTabRect.x, y2, innerTabRect.width, csh);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (innerTabRect.height > 0) {
|
||||||
|
float x2 = (tabPlacement == LEFT) ? x : x + w - csh;
|
||||||
|
gap = new Rectangle2D.Float(x2, innerTabRect.y, csh, innerTabRect.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gap != null) {
|
||||||
|
path.append(gap, false);
|
||||||
|
|
||||||
|
// fill gap in case that the tab is colored (e.g. focused or hover)
|
||||||
|
Color background = getTabBackground(tabPlacement, selectedIndex, true);
|
||||||
|
g.setColor(FlatUIUtils.deriveColor(background, tabPane.getBackground()));
|
||||||
|
((Graphics2D) g).fill(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// paint content separator or full border
|
||||||
|
g.setColor(contentAreaColor);
|
||||||
|
((Graphics2D) g).fill(path);
|
||||||
|
|
||||||
|
// repaint selection in scroll-tab-layout because it may be painted before
|
||||||
|
// the content border was painted (from BasicTabbedPaneUI$ScrollableTabPanel)
|
||||||
|
if (isScrollTabLayout() && selectedIndex >= 0 && tabViewport != null) {
|
||||||
|
Rectangle tabRect = getTabBounds(tabPane, selectedIndex);
|
||||||
|
|
||||||
|
// clip to "scrolling sides" of viewport
|
||||||
|
// (left and right if horizontal, top and bottom if vertical)
|
||||||
|
Shape oldClip = g.getClip();
|
||||||
|
Rectangle vr = tabViewport.getBounds();
|
||||||
|
if (isHorizontalTabPlacement(tabPlacement))
|
||||||
|
g.clipRect(vr.x, 0, vr.width, tabPane.getHeight());
|
||||||
|
else
|
||||||
|
g.clipRect(0, vr.y, tabPane.getWidth(), vr.height);
|
||||||
|
|
||||||
|
paintTabSelection(g, tabPlacement, selectedIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height);
|
||||||
|
g.setClip(oldClip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean isScrollTabLayout() {
|
||||||
|
return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
src/main/java/app/termora/MyKernel32.java
Normal file
38
src/main/java/app/termora/MyKernel32.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package app.termora;
|
||||||
|
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import com.sun.jna.WString;
|
||||||
|
import com.sun.jna.win32.StdCallLibrary;
|
||||||
|
|
||||||
|
interface MyKernel32 extends StdCallLibrary {
|
||||||
|
|
||||||
|
MyKernel32 INSTANCE = Native.load("Kernel32", MyKernel32.class);
|
||||||
|
WString INVARIANT_LOCALE = new WString("");
|
||||||
|
|
||||||
|
int CompareStringEx(WString lpLocaleName,
|
||||||
|
int dwCmpFlags,
|
||||||
|
WString lpString1,
|
||||||
|
int cchCount1,
|
||||||
|
WString lpString2,
|
||||||
|
int cchCount2,
|
||||||
|
Pointer lpVersionInformation,
|
||||||
|
Pointer lpReserved,
|
||||||
|
int lParam);
|
||||||
|
|
||||||
|
default int CompareStringEx(int dwCmpFlags,
|
||||||
|
String str1,
|
||||||
|
String str2) {
|
||||||
|
return MyKernel32.INSTANCE
|
||||||
|
.CompareStringEx(
|
||||||
|
INVARIANT_LOCALE,
|
||||||
|
dwCmpFlags,
|
||||||
|
new WString(str1),
|
||||||
|
str1.length(),
|
||||||
|
new WString(str2),
|
||||||
|
str2.length(),
|
||||||
|
Pointer.NULL,
|
||||||
|
Pointer.NULL,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,6 @@ package app.termora
|
|||||||
|
|
||||||
object Actions {
|
object Actions {
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将命令发送到多个会话
|
|
||||||
*/
|
|
||||||
const val MULTIPLE = "MultipleAction"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关键词高亮
|
* 关键词高亮
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import org.slf4j.LoggerFactory
|
|||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
@@ -60,6 +62,16 @@ object Application {
|
|||||||
return "/bin/bash"
|
return "/bin/bash"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTemporaryDir(): File {
|
||||||
|
val temporaryDir = File(getBaseDataDir(), "temporary")
|
||||||
|
FileUtils.forceMkdir(temporaryDir)
|
||||||
|
return temporaryDir
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSubTemporaryDir(prefix: String = getName()): Path {
|
||||||
|
return Files.createTempDirectory(getTemporaryDir().toPath(), prefix)
|
||||||
|
}
|
||||||
|
|
||||||
fun getBaseDataDir(): File {
|
fun getBaseDataDir(): File {
|
||||||
if (::baseDataDir.isInitialized) {
|
if (::baseDataDir.isInitialized) {
|
||||||
return baseDataDir
|
return baseDataDir
|
||||||
@@ -111,11 +123,18 @@ object Application {
|
|||||||
return "Termora"
|
return "Termora"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
fun browse(uri: URI, async: Boolean = true) {
|
fun browse(uri: URI, async: Boolean = true) {
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
// https://github.com/TermoraDev/termora/issues/178
|
||||||
|
if (SystemInfo.isWindows && uri.scheme == "file") {
|
||||||
|
if (async) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
|
} else {
|
||||||
|
tryBrowse(uri)
|
||||||
|
}
|
||||||
|
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
Desktop.getDesktop().browse(uri)
|
Desktop.getDesktop().browse(uri)
|
||||||
} else if (async) {
|
} else if (async) {
|
||||||
@Suppress("OPT_IN_USAGE")
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
GlobalScope.launch(Dispatchers.IO) { tryBrowse(uri) }
|
||||||
} else {
|
} else {
|
||||||
tryBrowse(uri)
|
tryBrowse(uri)
|
||||||
@@ -131,6 +150,16 @@ object Application {
|
|||||||
ProcessBuilder("xdg-open", uri.toString()).start()
|
ProcessBuilder("xdg-open", uri.toString()).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun browseInFolder(file: File) {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
ProcessBuilder("explorer", "/select," + file.absolutePath).start()
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
ProcessBuilder("open", "-R", file.absolutePath).start()
|
||||||
|
} else if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
|
||||||
|
Desktop.getDesktop().browseFileDirectory(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatBytes(bytes: Long): String {
|
fun formatBytes(bytes: Long): String {
|
||||||
@@ -140,7 +169,7 @@ fun formatBytes(bytes: Long): String {
|
|||||||
val exp = (ln(bytes.toDouble()) / ln(1024.0)).toInt()
|
val exp = (ln(bytes.toDouble()) / ln(1024.0)).toInt()
|
||||||
val value = bytes / 1024.0.pow(exp.toDouble())
|
val value = bytes / 1024.0.pow(exp.toDouble())
|
||||||
|
|
||||||
return String.format("%.2f %s", value, units[exp])
|
return String.format("%.2f%s", value, units[exp])
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatSeconds(seconds: Long): String {
|
fun formatSeconds(seconds: Long): String {
|
||||||
@@ -149,11 +178,33 @@ fun formatSeconds(seconds: Long): String {
|
|||||||
val minutes = (seconds % 3600) / 60
|
val minutes = (seconds % 3600) / 60
|
||||||
val remainingSeconds = seconds % 60
|
val remainingSeconds = seconds % 60
|
||||||
|
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
days > 0 -> "${days}天${hours}小时${minutes}分${remainingSeconds}秒"
|
days > 0 -> I18n.getString(
|
||||||
hours > 0 -> "${hours}小时${minutes}分${remainingSeconds}秒"
|
"termora.transport.jobs.table.estimated-time-days-format",
|
||||||
minutes > 0 -> "${minutes}分${remainingSeconds}秒"
|
days,
|
||||||
else -> "${remainingSeconds}秒"
|
hours,
|
||||||
|
minutes,
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
hours > 0 -> I18n.getString(
|
||||||
|
"termora.transport.jobs.table.estimated-time-hours-format",
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
minutes > 0 -> I18n.getString(
|
||||||
|
"termora.transport.jobs.table.estimated-time-minutes-format",
|
||||||
|
minutes,
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> I18n.getString(
|
||||||
|
"termora.transport.jobs.table.estimated-time-seconds-format",
|
||||||
|
remainingSeconds
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal file
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.pty4j.util.PtyUtil
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.tinylog.configuration.Configuration
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class ApplicationInitializr {
|
||||||
|
|
||||||
|
fun run() {
|
||||||
|
|
||||||
|
// 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
setupNativeLibraries()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
System.setProperty("apple.awt.application.name", Application.getName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 tinylog
|
||||||
|
setupTinylog()
|
||||||
|
|
||||||
|
// 检查是否单例
|
||||||
|
checkSingleton()
|
||||||
|
|
||||||
|
// 启动
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
val jSerialComm = FileUtils.getFile(dylib, "jSerialComm")
|
||||||
|
if (jSerialComm.exists()) {
|
||||||
|
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter")
|
||||||
|
if (restart4j.exists()) {
|
||||||
|
System.setProperty("restarter.path", restart4j.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows 情况覆盖
|
||||||
|
*/
|
||||||
|
private fun setupTinylog() {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
val dir = File(Application.getBaseDataDir(), "logs")
|
||||||
|
FileUtils.forceMkdir(dir)
|
||||||
|
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
||||||
|
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkSingleton() {
|
||||||
|
if (ApplicationSingleton.getInstance().isSingleton()) return
|
||||||
|
System.err.println("Program is already running")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ import app.termora.actions.ActionManager
|
|||||||
import app.termora.keymap.KeymapManager
|
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.FlatDesktop
|
||||||
import com.formdev.flatlaf.extras.FlatInspector
|
import com.formdev.flatlaf.extras.FlatInspector
|
||||||
|
import com.formdev.flatlaf.ui.FlatTableCellBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
import com.mixpanel.mixpanelapi.ClientDelivery
|
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||||
@@ -19,34 +21,16 @@ import org.apache.commons.lang3.LocaleUtils
|
|||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.tinylog.configuration.Configuration
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.channels.FileChannel
|
|
||||||
import java.nio.channels.FileLock
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.nio.file.StandardOpenOption
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class ApplicationRunner {
|
class ApplicationRunner {
|
||||||
private lateinit var singletonChannel: FileChannel
|
private val log by lazy { LoggerFactory.getLogger(ApplicationRunner::class.java) }
|
||||||
private lateinit var singletonLock: FileLock
|
|
||||||
private val log by lazy {
|
|
||||||
if (!::singletonLock.isInitialized) {
|
|
||||||
throw UnsupportedOperationException("Singleton lock is not initialized")
|
|
||||||
}
|
|
||||||
LoggerFactory.getLogger(ApplicationRunner::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun run() {
|
fun run() {
|
||||||
measureTimeMillis {
|
measureTimeMillis {
|
||||||
// 覆盖 tinylog 配置
|
|
||||||
val setupTinylog = measureTimeMillis { setupTinylog() }
|
|
||||||
|
|
||||||
// 是否单例
|
|
||||||
val checkSingleton = measureTimeMillis { checkSingleton() }
|
|
||||||
|
|
||||||
// 打印系统信息
|
// 打印系统信息
|
||||||
val printSystemInfo = measureTimeMillis { printSystemInfo() }
|
val printSystemInfo = measureTimeMillis { printSystemInfo() }
|
||||||
@@ -73,12 +57,13 @@ class ApplicationRunner {
|
|||||||
// 解密数据
|
// 解密数据
|
||||||
val openDoor = measureTimeMillis { openDoor() }
|
val openDoor = measureTimeMillis { openDoor() }
|
||||||
|
|
||||||
|
// clear temporary
|
||||||
|
clearTemporary()
|
||||||
|
|
||||||
// 启动主窗口
|
// 启动主窗口
|
||||||
val startMainFrame = measureTimeMillis { startMainFrame() }
|
val startMainFrame = measureTimeMillis { startMainFrame() }
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("setupTinylog: {}ms", setupTinylog)
|
|
||||||
log.debug("checkSingleton: {}ms", checkSingleton)
|
|
||||||
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
log.debug("printSystemInfo: {}ms", printSystemInfo)
|
||||||
log.debug("openDatabase: {}ms", openDatabase)
|
log.debug("openDatabase: {}ms", openDatabase)
|
||||||
log.debug("loadSettings: {}ms", loadSettings)
|
log.debug("loadSettings: {}ms", loadSettings)
|
||||||
@@ -94,6 +79,14 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
private fun clearTemporary() {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
// 启动时清除
|
||||||
|
FileUtils.cleanDirectory(Application.getTemporaryDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private fun openDoor() {
|
private fun openDoor() {
|
||||||
if (Doorman.getInstance().isWorking()) {
|
if (Doorman.getInstance().isWorking()) {
|
||||||
@@ -104,7 +97,19 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startMainFrame() {
|
private fun startMainFrame() {
|
||||||
|
|
||||||
|
|
||||||
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
TermoraFrameManager.getInstance().createWindow().isVisible = true
|
||||||
|
|
||||||
|
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||||
|
SwingUtilities.invokeLater { FlatDesktop.setQuitHandler { quitHandler() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun quitHandler() {
|
||||||
|
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
frame.dispose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
@@ -119,7 +124,7 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
private fun setupLaf() {
|
private fun setupLaf() {
|
||||||
|
|
||||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux}")
|
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
if (SystemInfo.isLinux) {
|
||||||
@@ -141,8 +146,9 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
themeManager.change(theme, true)
|
themeManager.change(theme, true)
|
||||||
|
|
||||||
|
|
||||||
if (Application.isUnknownVersion())
|
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)
|
||||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||||
@@ -173,9 +179,8 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UIManager.put("Table.rowHeight", 24)
|
UIManager.put("Table.rowHeight", 24)
|
||||||
UIManager.put("Table.cellNoFocusBorder", BorderFactory.createEmptyBorder())
|
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.focusCellHighlightBorder", BorderFactory.createEmptyBorder())
|
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||||
UIManager.put("Table.focusSelectedCellHighlightBorder", BorderFactory.createEmptyBorder())
|
|
||||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
|
||||||
UIManager.put("Tree.rowHeight", 24)
|
UIManager.put("Tree.rowHeight", 24)
|
||||||
@@ -209,36 +214,6 @@ class ApplicationRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Windows 情况覆盖
|
|
||||||
*/
|
|
||||||
private fun setupTinylog() {
|
|
||||||
if (SystemInfo.isWindows) {
|
|
||||||
val dir = File(Application.getBaseDataDir(), "logs")
|
|
||||||
FileUtils.forceMkdir(dir)
|
|
||||||
Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log")
|
|
||||||
Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun checkSingleton() {
|
|
||||||
singletonChannel = FileChannel.open(
|
|
||||||
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
|
||||||
StandardOpenOption.CREATE,
|
|
||||||
StandardOpenOption.WRITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
val lock = singletonChannel.tryLock()
|
|
||||||
if (lock == null) {
|
|
||||||
System.err.println("Program is already running")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
singletonLock = lock
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun openDatabase() {
|
private fun openDatabase() {
|
||||||
try {
|
try {
|
||||||
Database.getDatabase()
|
Database.getDatabase()
|
||||||
|
|||||||
201
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal file
201
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.sun.jna.platform.win32.Kernel32
|
||||||
|
import com.sun.jna.platform.win32.User32
|
||||||
|
import com.sun.jna.platform.win32.WinDef.*
|
||||||
|
import com.sun.jna.platform.win32.WinError
|
||||||
|
import com.sun.jna.platform.win32.WinUser.*
|
||||||
|
import com.sun.jna.platform.win32.Wtsapi32
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class ApplicationSingleton private constructor() : Disposable {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isSingleton = null as Boolean?
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): ApplicationSingleton {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(ApplicationSingleton::class) { ApplicationSingleton() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSingleton(): Boolean {
|
||||||
|
var singleton = this.isSingleton
|
||||||
|
if (singleton != null) return singleton
|
||||||
|
|
||||||
|
try {
|
||||||
|
synchronized(this) {
|
||||||
|
singleton = this.isSingleton
|
||||||
|
if (singleton != null) return singleton as Boolean
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
val handle = Kernel32.INSTANCE.CreateMutex(null, false, Application.getName())
|
||||||
|
singleton = handle != null && Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS
|
||||||
|
if (singleton == true) {
|
||||||
|
// 启动监听器,方便激活窗口
|
||||||
|
Thread.ofVirtual().start(Win32HelperWindow.getInstance())
|
||||||
|
} else {
|
||||||
|
// 尝试激活窗口
|
||||||
|
Win32HelperWindow.tick()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
singleton = FileLocker.getInstance().tryLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSingleton = singleton == true
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace(System.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return this.isSingleton == true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FileLocker private constructor() {
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): FileLocker {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(FileLocker::class) { FileLocker() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private lateinit var singletonChannel: FileChannel
|
||||||
|
private lateinit var singletonLock: FileLock
|
||||||
|
|
||||||
|
|
||||||
|
fun tryLock(): Boolean {
|
||||||
|
singletonChannel = FileChannel.open(
|
||||||
|
Paths.get(Application.getBaseDataDir().absolutePath, "lock"),
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
val lock = singletonChannel.tryLock() ?: return false
|
||||||
|
|
||||||
|
this.singletonLock = lock
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Win32HelperWindow private constructor() : Runnable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(Win32HelperWindow::class.java)
|
||||||
|
private val WindowClass = "${Application.getName()}HelperWindowClass"
|
||||||
|
private val WindowName =
|
||||||
|
"${Application.getName()} hidden helper window, used only to catch the windows events"
|
||||||
|
private const val TICK: Int = WM_USER + 1
|
||||||
|
|
||||||
|
fun getInstance(): Win32HelperWindow {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(Win32HelperWindow::class) { Win32HelperWindow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun tick() {
|
||||||
|
val hWnd = User32.INSTANCE.FindWindow(WindowClass, WindowName) ?: return
|
||||||
|
User32.INSTANCE.SendMessage(hWnd, TICK, WPARAM(), LPARAM())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isRunning = AtomicBoolean(false)
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
if (isRunning.compareAndSet(false, true)) {
|
||||||
|
Win32Window()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Win32Window : WindowProc {
|
||||||
|
/**
|
||||||
|
* Instantiates a new win32 window test.
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
// define new window class
|
||||||
|
val hInst = Kernel32.INSTANCE.GetModuleHandle(null)
|
||||||
|
|
||||||
|
val wClass = WNDCLASSEX()
|
||||||
|
wClass.hInstance = hInst
|
||||||
|
wClass.lpfnWndProc = this
|
||||||
|
wClass.lpszClassName = WindowClass
|
||||||
|
|
||||||
|
// register window class
|
||||||
|
User32.INSTANCE.RegisterClassEx(wClass)
|
||||||
|
|
||||||
|
// create new window
|
||||||
|
val hWnd = User32.INSTANCE.CreateWindowEx(
|
||||||
|
User32.WS_EX_TOPMOST,
|
||||||
|
WindowClass,
|
||||||
|
WindowName,
|
||||||
|
0, 0, 0, 0, 0,
|
||||||
|
null, // WM_DEVICECHANGE contradicts parent=WinUser.HWND_MESSAGE
|
||||||
|
null, hInst, null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val msg = MSG()
|
||||||
|
while (User32.INSTANCE.GetMessage(msg, hWnd, 0, 0) > 0) {
|
||||||
|
User32.INSTANCE.TranslateMessage(msg)
|
||||||
|
User32.INSTANCE.DispatchMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
Wtsapi32.INSTANCE.WTSUnRegisterSessionNotification(hWnd)
|
||||||
|
User32.INSTANCE.UnregisterClass(WindowClass, hInst)
|
||||||
|
User32.INSTANCE.DestroyWindow(hWnd)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun callback(hwnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT {
|
||||||
|
when (uMsg) {
|
||||||
|
WM_CREATE -> {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("win32 helper window created")
|
||||||
|
}
|
||||||
|
return LRESULT()
|
||||||
|
}
|
||||||
|
|
||||||
|
TICK -> {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("win32 helper window tick")
|
||||||
|
}
|
||||||
|
onTick()
|
||||||
|
return LRESULT()
|
||||||
|
}
|
||||||
|
|
||||||
|
WM_DESTROY -> {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("win32 helper window destroyed")
|
||||||
|
}
|
||||||
|
User32.INSTANCE.PostQuitMessage(0)
|
||||||
|
return LRESULT()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return User32.INSTANCE.DefWindowProc(hwnd, uMsg, wParam, lParam)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTick() {
|
||||||
|
TermoraFrameManager.getInstance().tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import app.termora.highlight.KeywordHighlight
|
|||||||
import app.termora.keymap.Keymap
|
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.snippet.Snippet
|
||||||
import app.termora.sync.SyncType
|
import app.termora.sync.SyncType
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import jetbrains.exodus.bindings.StringBinding
|
import jetbrains.exodus.bindings.StringBinding
|
||||||
@@ -12,14 +13,11 @@ import jetbrains.exodus.env.*
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.component1
|
|
||||||
import kotlin.collections.component2
|
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
@@ -28,6 +26,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val KEYMAP_STORE = "Keymap"
|
private const val KEYMAP_STORE = "Keymap"
|
||||||
private const val HOST_STORE = "Host"
|
private const val HOST_STORE = "Host"
|
||||||
|
private const val SNIPPET_STORE = "Snippet"
|
||||||
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"
|
||||||
@@ -55,6 +54,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
||||||
val terminal by lazy { Terminal() }
|
val terminal by lazy { Terminal() }
|
||||||
val appearance by lazy { Appearance() }
|
val appearance by lazy { Appearance() }
|
||||||
|
val sftp by lazy { SFTP() }
|
||||||
val sync by lazy { Sync() }
|
val sync by lazy { Sync() }
|
||||||
|
|
||||||
private val doorman get() = Doorman.getInstance()
|
private val doorman get() = Doorman.getInstance()
|
||||||
@@ -106,17 +106,6 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllHost() {
|
|
||||||
env.executeInTransaction { tx ->
|
|
||||||
val store = env.openStore(HOST_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
|
||||||
store.openCursor(tx).use {
|
|
||||||
while (it.next) {
|
|
||||||
it.deleteCurrent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllKeyPair() {
|
fun removeAllKeyPair() {
|
||||||
env.executeInTransaction { tx ->
|
env.executeInTransaction { tx ->
|
||||||
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||||
@@ -153,15 +142,32 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeHost(id: String) {
|
fun addSnippet(snippet: Snippet) {
|
||||||
|
var text = ohMyJson.encodeToString(snippet)
|
||||||
|
if (doorman.isWorking()) {
|
||||||
|
text = doorman.encrypt(text)
|
||||||
|
}
|
||||||
env.executeInTransaction {
|
env.executeInTransaction {
|
||||||
delete(it, HOST_STORE, id)
|
put(it, SNIPPET_STORE, snippet.id, text)
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Removed Host: $id")
|
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getSnippets(): Collection<Snippet> {
|
||||||
|
val isWorking = doorman.isWorking()
|
||||||
|
return env.computeInTransaction { tx ->
|
||||||
|
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
|
||||||
|
if (isWorking)
|
||||||
|
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||||
|
else
|
||||||
|
ohMyJson.decodeFromString(value)
|
||||||
|
}.values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
||||||
return env.computeInTransaction { tx ->
|
return env.computeInTransaction { tx ->
|
||||||
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
||||||
@@ -401,10 +407,10 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
|
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
|
||||||
PropertyDelegate<CursorStyle>(defaultValue) {
|
PropertyDelegate<CursorStyle>(defaultValue) {
|
||||||
override fun convertValue(value: String): CursorStyle {
|
override fun convertValue(value: String): CursorStyle {
|
||||||
try {
|
return try {
|
||||||
return CursorStyle.valueOf(value)
|
CursorStyle.valueOf(value)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
return initializer.invoke()
|
initializer.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,6 +460,16 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var debug by BooleanPropertyDelegate(false)
|
var debug by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 蜂鸣声
|
||||||
|
*/
|
||||||
|
var beep by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 光标闪烁
|
||||||
|
*/
|
||||||
|
var cursorBlink by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中复制
|
* 选中复制
|
||||||
*/
|
*/
|
||||||
@@ -463,6 +479,16 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 光标样式
|
* 光标样式
|
||||||
*/
|
*/
|
||||||
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
var cursor by CursorStylePropertyDelegate(CursorStyle.Block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终端断开连接时自动关闭Tab
|
||||||
|
*/
|
||||||
|
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示悬浮工具栏
|
||||||
|
*/
|
||||||
|
var floatingToolbar by BooleanPropertyDelegate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -563,6 +589,41 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SFTP
|
||||||
|
*/
|
||||||
|
inner class SFTP : Property("Setting.SFTP") {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑命令
|
||||||
|
*/
|
||||||
|
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sftp command
|
||||||
|
*/
|
||||||
|
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* defaultDirectory
|
||||||
|
*/
|
||||||
|
var defaultDirectory by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否固定在标签栏
|
||||||
|
*/
|
||||||
|
var pinTab by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否保留原始文件时间
|
||||||
|
*/
|
||||||
|
var preserveModificationTime by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步配置
|
* 同步配置
|
||||||
*/
|
*/
|
||||||
@@ -577,6 +638,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var rangeHosts by BooleanPropertyDelegate(true)
|
var rangeHosts by BooleanPropertyDelegate(true)
|
||||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||||
|
var rangeSnippets 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)
|
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.native.osx.NativeMacLibrary
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
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.*
|
import java.awt.*
|
||||||
@@ -12,29 +12,60 @@ 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 titleLabel = JLabel()
|
private val titleLabel = JLabel()
|
||||||
private val titleBar by lazy { LogicCustomTitleBar.createCustomTitleBar(this) }
|
|
||||||
val disposable = Disposer.newDisposable()
|
val disposable = Disposer.newDisposable()
|
||||||
|
private val customTitleBar = if (SystemInfo.isMacOS && JBR.isWindowDecorationsSupported())
|
||||||
|
JBR.getWindowDecorations().createCustomTitleBar() else null
|
||||||
|
|
||||||
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"
|
private const val PROCESS_GLOBAL_KEYMAP = "PROCESS_GLOBAL_KEYMAP"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected var controlsVisible = true
|
protected var controlsVisible = true
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
titleBar.putProperty("controls.visible", value)
|
if (SystemInfo.isMacOS) {
|
||||||
|
if (customTitleBar != null) {
|
||||||
|
customTitleBar.putProperty("controls.visible", value)
|
||||||
|
} else {
|
||||||
|
NativeMacLibrary.setControlsVisible(this, value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICONIFFY, value)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_MAXIMIZE, value)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
protected var fullWindowContent = false
|
||||||
set(value) {
|
set(value) {
|
||||||
titleBar.height = value
|
|
||||||
field = value
|
field = value
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var titleVisible = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var titleIconVisible = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected var titleBarHeight = UIManager.getInt("TabbedPane.tabHeight")
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
customTitleBar?.height = height.toFloat()
|
||||||
|
} else {
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected var lostFocusDispose = false
|
protected var lostFocusDispose = false
|
||||||
@@ -51,25 +82,43 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
|
super.rootPane.putClientProperty(PROCESS_GLOBAL_KEYMAP, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
super.setDefaultCloseOperation(DISPOSE_ON_CLOSE)
|
||||||
|
|
||||||
|
// 使用 FlatLaf 的 TitlePane
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected fun init() {
|
protected fun init() {
|
||||||
|
|
||||||
|
|
||||||
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
|
||||||
|
|
||||||
initTitleBar()
|
|
||||||
initEvents()
|
initEvents()
|
||||||
|
|
||||||
if (JBR.isWindowDecorationsSupported()) {
|
val rootPanel = JPanel(BorderLayout())
|
||||||
if (rootPane.getClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE) != false) {
|
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
||||||
val titlePanel = createTitlePanel()
|
|
||||||
if (titlePanel != null) {
|
if (SystemInfo.isMacOS) {
|
||||||
rootPanel.add(titlePanel, BorderLayout.NORTH)
|
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||||
}
|
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||||
|
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||||
|
rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||||
|
)
|
||||||
|
|
||||||
|
val titlePanel = createTitlePanel()
|
||||||
|
if (titlePanel != null) {
|
||||||
|
rootPanel.add(titlePanel, BorderLayout.NORTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
val customTitleBar = this.customTitleBar
|
||||||
|
if (customTitleBar != null) {
|
||||||
|
customTitleBar.putProperty("controls.visible", controlsVisible)
|
||||||
|
customTitleBar.height = titleBarHeight.toFloat()
|
||||||
|
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootPanel.add(createCenterPanel(), BorderLayout.CENTER)
|
|
||||||
|
|
||||||
val southPanel = createSouthPanel()
|
val southPanel = createSouthPanel()
|
||||||
if (southPanel != null) {
|
if (southPanel != null) {
|
||||||
rootPanel.add(southPanel, BorderLayout.SOUTH)
|
rootPanel.add(southPanel, BorderLayout.SOUTH)
|
||||||
@@ -122,7 +171,7 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
|
|
||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
panel.add(titleLabel, BorderLayout.CENTER)
|
panel.add(titleLabel, BorderLayout.CENTER)
|
||||||
panel.preferredSize = Dimension(-1, titleBar.height.toInt())
|
panel.preferredSize = Dimension(-1, titleBarHeight)
|
||||||
|
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
@@ -158,12 +207,14 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
openPopup = true
|
openPopup = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val window = SwingUtilities.windowForComponent(c)
|
val window = c as? Window ?: SwingUtilities.windowForComponent(c)
|
||||||
val windows = window.ownedWindows
|
if (window != null) {
|
||||||
for (w in windows) {
|
val windows = window.ownedWindows
|
||||||
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
|
for (w in windows) {
|
||||||
openPopup = true
|
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
|
||||||
w.dispose()
|
openPopup = true
|
||||||
|
w.dispose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,30 +240,20 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (SystemInfo.isWindows) {
|
|
||||||
addWindowListener(object : WindowAdapter(), ThemeChangeListener {
|
|
||||||
override fun windowClosed(e: WindowEvent) {
|
|
||||||
ThemeManager.getInstance().removeThemeChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun windowOpened(e: WindowEvent) {
|
|
||||||
onChanged()
|
|
||||||
ThemeManager.getInstance().addThemeChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChanged() {
|
|
||||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTitleBar() {
|
override fun addNotify() {
|
||||||
titleBar.height = titleBarHeight
|
super.addNotify()
|
||||||
titleBar.putProperty("controls.visible", controlsVisible)
|
|
||||||
if (JBR.isWindowDecorationsSupported()) {
|
// 显示后触发一次重绘制
|
||||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
this.controlsVisible = controlsVisible
|
||||||
|
this.titleBarHeight = titleBarHeight
|
||||||
|
this.titleIconVisible = titleIconVisible
|
||||||
|
this.titleVisible = titleVisible
|
||||||
|
this.fullWindowContent = fullWindowContent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun doOKAction() {
|
protected open fun doOKAction() {
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
|||||||
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
||||||
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
||||||
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
||||||
|
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHost(): Host {
|
override fun getHost(): Host {
|
||||||
|
|||||||
156
src/main/kotlin/app/termora/FilterableHostTreeModel.kt
Normal file
156
src/main/kotlin/app/termora/FilterableHostTreeModel.kt
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
|
import java.util.function.Function
|
||||||
|
import javax.swing.JTree
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import javax.swing.event.TreeModelEvent
|
||||||
|
import javax.swing.event.TreeModelListener
|
||||||
|
import javax.swing.tree.DefaultMutableTreeNode
|
||||||
|
import javax.swing.tree.TreeModel
|
||||||
|
import javax.swing.tree.TreeNode
|
||||||
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
|
class FilterableHostTreeModel(
|
||||||
|
private val tree: JTree,
|
||||||
|
/**
|
||||||
|
* 如果返回 true 则空文件夹也展示
|
||||||
|
*/
|
||||||
|
private val showEmptyFolder: () -> Boolean = { true }
|
||||||
|
) : TreeModel {
|
||||||
|
private val model = tree.model
|
||||||
|
private val root = ReferenceTreeNode(model.root)
|
||||||
|
private var listeners = emptyArray<TreeModelListener>()
|
||||||
|
private var filters = emptyArray<Function<HostTreeNode, Boolean>>()
|
||||||
|
private val mapping = mutableMapOf<TreeNode, ReferenceTreeNode>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
refresh()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param a 旧的
|
||||||
|
* @param b 新的
|
||||||
|
*/
|
||||||
|
private fun cloneTree(a: HostTreeNode, b: DefaultMutableTreeNode) {
|
||||||
|
b.removeAllChildren()
|
||||||
|
for (c in a.children()) {
|
||||||
|
if (c !is HostTreeNode) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.data.protocol != Protocol.Folder) {
|
||||||
|
if (filters.isNotEmpty() && filters.none { it.apply(c) }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val n = ReferenceTreeNode(c).apply { mapping[c] = this }.apply { b.add(this) }
|
||||||
|
|
||||||
|
// 文件夹递归复制
|
||||||
|
if (c.host.protocol == Protocol.Folder) {
|
||||||
|
cloneTree(c, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是文件夹
|
||||||
|
if (c.host.protocol == Protocol.Folder) {
|
||||||
|
if (n.childCount == 0) {
|
||||||
|
if (showEmptyFolder.invoke()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n.removeFromParent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
model.addTreeModelListener(object : TreeModelListener {
|
||||||
|
override fun treeNodesChanged(e: TreeModelEvent) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeNodesInserted(e: TreeModelEvent) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeNodesRemoved(e: TreeModelEvent) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeStructureChanged(e: TreeModelEvent) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRoot(): Any {
|
||||||
|
return root.userObject
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChild(parent: Any, index: Int): Any {
|
||||||
|
val c = map(parent)?.getChildAt(index)
|
||||||
|
if (c is ReferenceTreeNode) {
|
||||||
|
return c.userObject
|
||||||
|
}
|
||||||
|
throw IndexOutOfBoundsException("Index out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChildCount(parent: Any): Int {
|
||||||
|
return map(parent)?.childCount ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun map(parent: Any): ReferenceTreeNode? {
|
||||||
|
if (parent is TreeNode) {
|
||||||
|
return mapping[parent]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLeaf(node: Any?): Boolean {
|
||||||
|
return (node as TreeNode).isLeaf
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun valueForPathChanged(path: TreePath, newValue: Any) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIndexOfChild(parent: Any, child: Any): Int {
|
||||||
|
val c = map(parent) ?: return -1
|
||||||
|
for (i in 0 until c.childCount) {
|
||||||
|
val e = c.getChildAt(i)
|
||||||
|
if (e is ReferenceTreeNode && e.userObject == child) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTreeModelListener(l: TreeModelListener) {
|
||||||
|
listeners = ArrayUtils.addAll(listeners, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeTreeModelListener(l: TreeModelListener) {
|
||||||
|
listeners = ArrayUtils.removeElement(listeners, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFilter(f: Function<HostTreeNode, Boolean>) {
|
||||||
|
filters = ArrayUtils.add(filters, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
mapping.clear()
|
||||||
|
mapping[model.root as TreeNode] = root
|
||||||
|
cloneTree(model.root as HostTreeNode, root)
|
||||||
|
SwingUtilities.updateComponentTreeUI(tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getModel(): TreeModel {
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReferenceTreeNode(any: Any) : DefaultMutableTreeNode(any)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,17 @@ import org.apache.commons.lang3.StringUtils
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
fun Map<*, *>.toPropertiesString(): String {
|
||||||
|
val env = StringBuilder()
|
||||||
|
for ((i, e) in entries.withIndex()) {
|
||||||
|
env.append(e.key).append('=').append(e.value)
|
||||||
|
if (i != size - 1) {
|
||||||
|
env.appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return env.toString()
|
||||||
|
}
|
||||||
|
|
||||||
fun UUID.toSimpleString(): String {
|
fun UUID.toSimpleString(): String {
|
||||||
return toString().replace("-", StringUtils.EMPTY)
|
return toString().replace("-", StringUtils.EMPTY)
|
||||||
}
|
}
|
||||||
@@ -13,7 +24,13 @@ enum class Protocol {
|
|||||||
Folder,
|
Folder,
|
||||||
SSH,
|
SSH,
|
||||||
Local,
|
Local,
|
||||||
Serial
|
Serial,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交互式的 SFTP,此协议只在系统内部交互不应该暴露给用户也不应该持久化
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
SFTPPty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -115,6 +132,11 @@ data class Options(
|
|||||||
* 串口配置
|
* 串口配置
|
||||||
*/
|
*/
|
||||||
val serialComm: SerialComm = SerialComm(),
|
val serialComm: SerialComm = SerialComm(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SFTP 默认目录
|
||||||
|
*/
|
||||||
|
val sftpDefaultDirectory: String = StringUtils.EMPTY,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = Options()
|
val Default = Options()
|
||||||
@@ -243,7 +265,7 @@ data class Host(
|
|||||||
val tunnelings: List<Tunneling> = emptyList(),
|
val tunnelings: List<Tunneling> = emptyList(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排序
|
* 排序,越小越靠前
|
||||||
*/
|
*/
|
||||||
val sort: Long = 0,
|
val sort: Long = 0,
|
||||||
/**
|
/**
|
||||||
@@ -290,4 +312,8 @@ data class Host(
|
|||||||
result = 31 * result + ownerId.hashCode()
|
result = 31 * result + ownerId.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface HostListener : EventListener {
|
|
||||||
fun hostAdded(host: Host) {}
|
|
||||||
fun hostRemoved(id: String) {}
|
|
||||||
fun hostsChanged() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HostManager private constructor() {
|
class HostManager private constructor() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -17,39 +9,38 @@ class HostManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
private val listeners = mutableListOf<HostListener>()
|
private var hosts = mutableMapOf<String, Host>()
|
||||||
|
|
||||||
fun addHost(host: Host, notify: Boolean = true) {
|
/**
|
||||||
|
* 修改缓存并存入数据库
|
||||||
|
*/
|
||||||
|
fun addHost(host: Host) {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
database.addHost(host)
|
database.addHost(host)
|
||||||
if (notify) listeners.forEach { it.hostAdded(host) }
|
if (host.deleted) {
|
||||||
}
|
hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id }
|
||||||
|
} else {
|
||||||
fun removeHost(id: String) {
|
hosts[host.id] = host
|
||||||
assertEventDispatchThread()
|
}
|
||||||
database.removeHost(id)
|
|
||||||
listeners.forEach { it.hostRemoved(id) }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第一次调用从数据库中获取,后续从缓存中获取
|
||||||
|
*/
|
||||||
fun hosts(): List<Host> {
|
fun hosts(): List<Host> {
|
||||||
return database.getHosts()
|
if (hosts.isEmpty()) {
|
||||||
|
database.getHosts().filter { !it.deleted }
|
||||||
|
.forEach { hosts[it.id] = it }
|
||||||
|
}
|
||||||
|
return hosts.values.filter { !it.deleted }
|
||||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAll() {
|
/**
|
||||||
assertEventDispatchThread()
|
* 从缓存中获取
|
||||||
database.removeAllHost()
|
*/
|
||||||
listeners.forEach { it.hostsChanged() }
|
fun getHost(id: String): Host? {
|
||||||
|
return hosts[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addHostListener(listener: HostListener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeHostListener(listener: HostListener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
protected val terminalOption = TerminalOption()
|
protected val terminalOption = TerminalOption()
|
||||||
protected val jumpHostsOption = JumpHostsOption()
|
protected val jumpHostsOption = JumpHostsOption()
|
||||||
protected val serialCommOption = SerialCommOption()
|
protected val serialCommOption = SerialCommOption()
|
||||||
|
protected val sftpOption = SFTPOption()
|
||||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -38,6 +39,7 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
addOption(jumpHostsOption)
|
addOption(jumpHostsOption)
|
||||||
addOption(terminalOption)
|
addOption(terminalOption)
|
||||||
addOption(serialCommOption)
|
addOption(serialCommOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,8 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
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 },
|
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||||
serialComm = serialComm
|
serialComm = serialComm,
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text
|
||||||
)
|
)
|
||||||
|
|
||||||
return Host(
|
return Host(
|
||||||
@@ -669,6 +672,54 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return "SFTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, $formMargin",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||||
val tunnelings = mutableListOf<Tunneling>()
|
val tunnelings = mutableListOf<Tunneling>()
|
||||||
|
|
||||||
@@ -1134,16 +1185,16 @@ open class HostOptionsPane : OptionsPane() {
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addBtn.addActionListener(object : AbstractAction() {
|
addBtn.addActionListener(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent?) {
|
override fun actionPerformed(e: ActionEvent?) {
|
||||||
val dialog = HostTreeDialog(owner) { host ->
|
val dialog = NewHostTreeDialog(owner)
|
||||||
jumpHosts.none { it.id == host.id } && filter.invoke(host)
|
dialog.setFilter { node -> jumpHosts.none { it.id == node.host.id } && filter.invoke(node.host) }
|
||||||
}
|
dialog.setTreeName("HostOptionsPane.JumpHostsOption.Tree")
|
||||||
|
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val hosts = dialog.hosts
|
val hosts = dialog.hosts
|
||||||
if (hosts.isEmpty()) {
|
if (hosts.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts.forEach {
|
hosts.forEach {
|
||||||
val rowCount = model.rowCount
|
val rowCount = model.rowCount
|
||||||
jumpHosts.add(it)
|
jumpHosts.add(it)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -11,8 +13,8 @@ import javax.swing.Icon
|
|||||||
abstract class HostTerminalTab(
|
abstract class HostTerminalTab(
|
||||||
val windowScope: WindowScope,
|
val windowScope: WindowScope,
|
||||||
val host: Host,
|
val host: Host,
|
||||||
protected val terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
protected val terminal: Terminal = TerminalFactory.getInstance().createTerminal()
|
||||||
) : PropertyTerminalTab() {
|
) : PropertyTerminalTab(), DataProvider {
|
||||||
companion object {
|
companion object {
|
||||||
val Host = DataKey(app.termora.Host::class)
|
val Host = DataKey(app.termora.Host::class)
|
||||||
}
|
}
|
||||||
@@ -69,4 +71,11 @@ abstract class HostTerminalTab(
|
|||||||
unread = false
|
unread = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == DataProviders.Terminal) {
|
||||||
|
return terminal as T?
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,585 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
|
|
||||||
import app.termora.actions.NewHostAction
|
|
||||||
import app.termora.actions.OpenHostAction
|
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
|
||||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
|
||||||
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
|
||||||
import java.awt.Component
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.datatransfer.DataFlavor
|
|
||||||
import java.awt.datatransfer.Transferable
|
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.util.*
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.event.CellEditorListener
|
|
||||||
import javax.swing.event.ChangeEvent
|
|
||||||
import javax.swing.event.PopupMenuEvent
|
|
||||||
import javax.swing.event.PopupMenuListener
|
|
||||||
import javax.swing.tree.TreePath
|
|
||||||
import javax.swing.tree.TreeSelectionModel
|
|
||||||
|
|
||||||
|
|
||||||
class HostTree : JTree(), Disposable {
|
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
private val editor = OutlineTextField(64)
|
|
||||||
|
|
||||||
var contextmenu = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 双击是否打开连接
|
|
||||||
*/
|
|
||||||
var doubleClickConnection = true
|
|
||||||
|
|
||||||
val model = HostTreeModel()
|
|
||||||
val searchableModel = SearchableHostTreeModel(model)
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
setModel(model)
|
|
||||||
isEditable = true
|
|
||||||
dropMode = DropMode.ON_OR_INSERT
|
|
||||||
dragEnabled = true
|
|
||||||
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
|
||||||
editor.preferredSize = Dimension(220, 0)
|
|
||||||
|
|
||||||
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
|
||||||
override fun getTreeCellRendererComponent(
|
|
||||||
tree: JTree,
|
|
||||||
value: Any,
|
|
||||||
sel: Boolean,
|
|
||||||
expanded: Boolean,
|
|
||||||
leaf: Boolean,
|
|
||||||
row: Int,
|
|
||||||
hasFocus: Boolean
|
|
||||||
): Component {
|
|
||||||
val host = value as Host
|
|
||||||
val c = super.getTreeCellRendererComponent(tree, host, sel, expanded, leaf, row, hasFocus)
|
|
||||||
if (host.protocol == Protocol.Folder) {
|
|
||||||
icon = if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
|
||||||
} else if (host.protocol == Protocol.SSH || host.protocol == Protocol.Local) {
|
|
||||||
icon = if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
|
||||||
} else if (host.protocol == Protocol.Serial) {
|
|
||||||
icon = if (sel && this@HostTree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setCellEditor(object : DefaultCellEditor(editor) {
|
|
||||||
override fun isCellEditable(e: EventObject?): Boolean {
|
|
||||||
if (e is MouseEvent) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return super.isCellEditable(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
val state = Database.getDatabase().properties.getString("HostTreeExpansionState")
|
|
||||||
if (state != null) {
|
|
||||||
TreeUtils.loadExpansionState(this@HostTree, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convertValueToText(
|
|
||||||
value: Any?,
|
|
||||||
selected: Boolean,
|
|
||||||
expanded: Boolean,
|
|
||||||
leaf: Boolean,
|
|
||||||
row: Int,
|
|
||||||
hasFocus: Boolean
|
|
||||||
): String {
|
|
||||||
if (value is Host) {
|
|
||||||
return value.name
|
|
||||||
}
|
|
||||||
return super.convertValueToText(value, selected, expanded, leaf, row, hasFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvents() {
|
|
||||||
// 右键选中
|
|
||||||
addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestFocusInWindow()
|
|
||||||
|
|
||||||
val selectionRows = selectionModel.selectionRows
|
|
||||||
|
|
||||||
val selRow = getClosestRowForLocation(e.x, e.y)
|
|
||||||
if (selRow < 0) {
|
|
||||||
selectionModel.clearSelection()
|
|
||||||
return
|
|
||||||
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionPath = getPathForLocation(e.x, e.y)
|
|
||||||
|
|
||||||
setSelectionRow(selRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
|
||||||
val host = lastSelectedPathComponent
|
|
||||||
if (host is Host && host.protocol != Protocol.Folder) {
|
|
||||||
ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
|
||||||
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// contextmenu
|
|
||||||
addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (!(SwingUtilities.isRightMouseButton(e))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Objects.isNull(lastSelectedPathComponent)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SwingUtilities.invokeLater { showContextMenu(e) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// rename
|
|
||||||
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
|
||||||
override fun editingStopped(e: ChangeEvent) {
|
|
||||||
val lastHost = lastSelectedPathComponent
|
|
||||||
if (lastHost !is Host || editor.text.isBlank() || editor.text == lastHost.name) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runCatchingHost(lastHost.copy(name = editor.text))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun editingCanceled(e: ChangeEvent) {
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// drag
|
|
||||||
transferHandler = object : TransferHandler() {
|
|
||||||
|
|
||||||
override fun createTransferable(c: JComponent): Transferable {
|
|
||||||
val nodes = selectionModel.selectionPaths
|
|
||||||
.map { it.lastPathComponent }
|
|
||||||
.filterIsInstance<Host>()
|
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
val iterator = nodes.iterator()
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
val node = iterator.next()
|
|
||||||
val parents = model.getPathToRoot(node).filter { it != node }
|
|
||||||
if (parents.any { nodes.contains(it) }) {
|
|
||||||
iterator.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MoveHostTransferable(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSourceActions(c: JComponent?): Int {
|
|
||||||
return MOVE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canImport(support: TransferSupport): Boolean {
|
|
||||||
if (!support.isDrop) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val dropLocation = support.dropLocation
|
|
||||||
if (dropLocation !is JTree.DropLocation || support.component != this@HostTree
|
|
||||||
|| dropLocation.childIndex != -1
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastNode = dropLocation.path.lastPathComponent
|
|
||||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
|
||||||
val nodes = support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>
|
|
||||||
if (nodes.any { it == lastNode }) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (parent in model.getPathToRoot(lastNode).filter { it != lastNode }) {
|
|
||||||
if (nodes.any { it == parent }) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
support.setShowDropLocation(true)
|
|
||||||
return support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun importData(support: TransferSupport): Boolean {
|
|
||||||
if (!support.isDrop) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val dropLocation = support.dropLocation
|
|
||||||
if (dropLocation !is JTree.DropLocation) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastNode = dropLocation.path.lastPathComponent
|
|
||||||
if (lastNode !is Host || lastNode.protocol != Protocol.Folder) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!support.isDataFlavorSupported(MoveHostTransferable.dataFlavor)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val hosts = (support.transferable.getTransferData(MoveHostTransferable.dataFlavor) as List<*>)
|
|
||||||
.filterIsInstance<Host>().toMutableList()
|
|
||||||
if (hosts.isEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录展开的节点
|
|
||||||
val expandedHosts = mutableListOf<String>()
|
|
||||||
for (host in hosts) {
|
|
||||||
model.visit(host) {
|
|
||||||
if (it.protocol == Protocol.Folder) {
|
|
||||||
if (isExpanded(TreePath(model.getPathToRoot(it)))) {
|
|
||||||
expandedHosts.addFirst(it.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = System.currentTimeMillis()
|
|
||||||
for (host in hosts) {
|
|
||||||
model.removeNodeFromParent(host)
|
|
||||||
val newHost = host.copy(
|
|
||||||
parentId = lastNode.id,
|
|
||||||
sort = ++now,
|
|
||||||
updateDate = now
|
|
||||||
)
|
|
||||||
runCatchingHost(newHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
expandNode(lastNode)
|
|
||||||
|
|
||||||
// 展开
|
|
||||||
for (id in expandedHosts) {
|
|
||||||
model.getHost(id)?.let { expandNode(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isPathEditable(path: TreePath?): Boolean {
|
|
||||||
if (path == null) return false
|
|
||||||
if (path.lastPathComponent == model.root) return false
|
|
||||||
return super.isPathEditable(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLastSelectedPathComponent(): Any? {
|
|
||||||
val last = super.getLastSelectedPathComponent() ?: return null
|
|
||||||
if (last is Host) {
|
|
||||||
return model.getHost(last.id) ?: last
|
|
||||||
}
|
|
||||||
return last
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showContextMenu(event: MouseEvent) {
|
|
||||||
if (!contextmenu) return
|
|
||||||
|
|
||||||
val lastHost = lastSelectedPathComponent
|
|
||||||
if (lastHost !is Host) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val popupMenu = FlatPopupMenu()
|
|
||||||
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
|
||||||
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
|
||||||
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
|
||||||
|
|
||||||
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()
|
|
||||||
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
|
||||||
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
|
||||||
val rename = popupMenu.add(I18n.getString("termora.welcome.contextmenu.rename"))
|
|
||||||
popupMenu.addSeparator()
|
|
||||||
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
|
|
||||||
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
|
|
||||||
popupMenu.addSeparator()
|
|
||||||
popupMenu.add(newMenu)
|
|
||||||
popupMenu.addSeparator()
|
|
||||||
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
|
||||||
|
|
||||||
open.addActionListener { openHosts(it, false) }
|
|
||||||
openInNewWindow.addActionListener { openHosts(it, true) }
|
|
||||||
|
|
||||||
rename.addActionListener {
|
|
||||||
startEditingAtPath(TreePath(model.getPathToRoot(lastHost)))
|
|
||||||
}
|
|
||||||
|
|
||||||
expandAll.addActionListener {
|
|
||||||
getSelectionNodes().forEach { expandNode(it, true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
colspanAll.addActionListener {
|
|
||||||
selectionModel.selectionPaths.map { it.lastPathComponent }
|
|
||||||
.filterIsInstance<Host>()
|
|
||||||
.filter { it.protocol == Protocol.Folder }
|
|
||||||
.forEach { collapseNode(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
copy.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
val parent = model.getParent(lastHost) ?: return
|
|
||||||
val node = copyNode(parent, lastHost)
|
|
||||||
selectionPath = TreePath(model.getPathToRoot(node))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
remove.addActionListener {
|
|
||||||
if (OptionPane.showConfirmDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(this),
|
|
||||||
I18n.getString("termora.keymgr.delete-warning"),
|
|
||||||
I18n.getString("termora.remove"),
|
|
||||||
JOptionPane.YES_NO_OPTION,
|
|
||||||
JOptionPane.QUESTION_MESSAGE
|
|
||||||
) == JOptionPane.YES_OPTION
|
|
||||||
) {
|
|
||||||
var lastParent: Host? = null
|
|
||||||
while (!selectionModel.isSelectionEmpty) {
|
|
||||||
val host = lastSelectedPathComponent ?: break
|
|
||||||
if (host !is Host) {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
lastParent = model.getParent(host)
|
|
||||||
}
|
|
||||||
model.visit(host) { hostManager.removeHost(it.id) }
|
|
||||||
}
|
|
||||||
if (lastParent != null) {
|
|
||||||
selectionPath = TreePath(model.getPathToRoot(lastParent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newFolder.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
if (lastHost.protocol != Protocol.Folder) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val host = Host(
|
|
||||||
id = UUID.randomUUID().toSimpleString(),
|
|
||||||
protocol = Protocol.Folder,
|
|
||||||
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
|
||||||
sort = System.currentTimeMillis(),
|
|
||||||
parentId = lastHost.id
|
|
||||||
)
|
|
||||||
|
|
||||||
runCatchingHost(host)
|
|
||||||
|
|
||||||
expandNode(lastHost)
|
|
||||||
selectionPath = TreePath(model.getPathToRoot(host))
|
|
||||||
startEditingAtPath(selectionPath)
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
newHost.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
ActionManager.getInstance().getAction(NewHostAction.NEW_HOST)
|
|
||||||
?.actionPerformed(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
property.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
val dialog = HostDialog(SwingUtilities.getWindowAncestor(this@HostTree), lastHost)
|
|
||||||
dialog.isVisible = true
|
|
||||||
val host = dialog.host ?: return
|
|
||||||
runCatchingHost(host)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化状态
|
|
||||||
newFolder.isEnabled = lastHost.protocol == Protocol.Folder
|
|
||||||
newHost.isEnabled = newFolder.isEnabled
|
|
||||||
remove.isEnabled = !getSelectionNodes().any { it == model.root }
|
|
||||||
copy.isEnabled = remove.isEnabled
|
|
||||||
rename.isEnabled = remove.isEnabled
|
|
||||||
property.isEnabled = lastHost.protocol != Protocol.Folder
|
|
||||||
|
|
||||||
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
|
||||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
|
||||||
this@HostTree.grabFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
|
|
||||||
this@HostTree.requestFocusInWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popupMenuCanceled(e: PopupMenuEvent) {
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
popupMenu.show(this, event.x, event.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
|
||||||
assertEventDispatchThread()
|
|
||||||
val nodes = getSelectionNodes().filter { it.protocol != Protocol.Folder }
|
|
||||||
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)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun expandNode(node: Host, including: Boolean = false) {
|
|
||||||
expandPath(TreePath(model.getPathToRoot(node)))
|
|
||||||
if (including) {
|
|
||||||
model.getChildren(node).forEach { expandNode(it, true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun copyNode(
|
|
||||||
parent: Host,
|
|
||||||
host: Host,
|
|
||||||
idGenerator: () -> String = { UUID.randomUUID().toSimpleString() }
|
|
||||||
): Host {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val newHost = host.copy(
|
|
||||||
name = "${host.name} ${I18n.getString("termora.welcome.contextmenu.copy")}",
|
|
||||||
id = idGenerator.invoke(),
|
|
||||||
parentId = parent.id,
|
|
||||||
updateDate = now,
|
|
||||||
createDate = now,
|
|
||||||
sort = now
|
|
||||||
)
|
|
||||||
|
|
||||||
runCatchingHost(newHost)
|
|
||||||
|
|
||||||
if (host.protocol == Protocol.Folder) {
|
|
||||||
for (child in model.getChildren(host)) {
|
|
||||||
copyNode(newHost, child, idGenerator)
|
|
||||||
}
|
|
||||||
if (isExpanded(TreePath(model.getPathToRoot(host)))) {
|
|
||||||
expandNode(newHost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newHost
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runCatchingHost(host: Host) {
|
|
||||||
hostManager.addHost(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collapseNode(node: Host) {
|
|
||||||
model.getChildren(node).forEach { collapseNode(it) }
|
|
||||||
collapsePath(TreePath(model.getPathToRoot(node)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSelectionNodes(): List<Host> {
|
|
||||||
val selectionNodes = selectionModel.selectionPaths.map { it.lastPathComponent }
|
|
||||||
.filterIsInstance<Host>()
|
|
||||||
|
|
||||||
if (selectionNodes.isEmpty()) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nodes = mutableListOf<Host>()
|
|
||||||
val parents = mutableListOf<Host>()
|
|
||||||
|
|
||||||
for (node in selectionNodes) {
|
|
||||||
if (node.protocol == Protocol.Folder) {
|
|
||||||
parents.add(node)
|
|
||||||
}
|
|
||||||
nodes.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
while (parents.isNotEmpty()) {
|
|
||||||
val p = parents.removeFirst()
|
|
||||||
for (i in 0 until getModel().getChildCount(p)) {
|
|
||||||
val child = getModel().getChild(p, i) as Host
|
|
||||||
nodes.add(child)
|
|
||||||
parents.add(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保是最新的
|
|
||||||
for (i in 0 until nodes.size) {
|
|
||||||
nodes[i] = model.getHost(nodes[i].id) ?: continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
Database.getDatabase().properties.putString(
|
|
||||||
"HostTreeExpansionState",
|
|
||||||
TreeUtils.saveExpansionState(this)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract class HostTreeNodeTransferable(val hosts: List<Host>) :
|
|
||||||
Transferable {
|
|
||||||
|
|
||||||
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
|
||||||
return arrayOf(getDataFlavor())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDataFlavorSupported(flavor: DataFlavor): Boolean {
|
|
||||||
return getDataFlavor() == flavor
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTransferData(flavor: DataFlavor): Any {
|
|
||||||
return hosts
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun getDataFlavor(): DataFlavor
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MoveHostTransferable(hosts: List<Host>) : HostTreeNodeTransferable(hosts) {
|
|
||||||
companion object {
|
|
||||||
val dataFlavor =
|
|
||||||
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveHostTransferable::class.java.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataFlavor(): DataFlavor {
|
|
||||||
return dataFlavor
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.Window
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.awt.event.WindowAdapter
|
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.tree.TreeSelectionModel
|
|
||||||
|
|
||||||
class HostTreeDialog(
|
|
||||||
owner: Window,
|
|
||||||
private val filter: (host: Host) -> Boolean = { true }
|
|
||||||
) : DialogWrapper(owner) {
|
|
||||||
|
|
||||||
private val tree = HostTree()
|
|
||||||
|
|
||||||
val hosts = mutableListOf<Host>()
|
|
||||||
|
|
||||||
var allowMulti = true
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (value) {
|
|
||||||
tree.selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
|
||||||
} else {
|
|
||||||
tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
|
||||||
isModal = true
|
|
||||||
isResizable = false
|
|
||||||
controlsVisible = false
|
|
||||||
title = I18n.getString("termora.transport.sftp.select-host")
|
|
||||||
|
|
||||||
tree.setModel(SearchableHostTreeModel(tree.model) { host ->
|
|
||||||
(host.protocol == Protocol.Folder || host.protocol == Protocol.SSH) && filter.invoke(host)
|
|
||||||
})
|
|
||||||
tree.contextmenu = true
|
|
||||||
tree.doubleClickConnection = false
|
|
||||||
tree.dragEnabled = false
|
|
||||||
|
|
||||||
initEvents()
|
|
||||||
|
|
||||||
init()
|
|
||||||
setLocationRelativeTo(null)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvents() {
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowActivated(e: WindowEvent) {
|
|
||||||
removeWindowListener(this)
|
|
||||||
val state = Database.getDatabase().properties.getString("HostTreeDialog.HostTreeExpansionState")
|
|
||||||
if (state != null) {
|
|
||||||
TreeUtils.loadExpansionState(tree, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
tree.addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
|
||||||
val node = tree.lastSelectedPathComponent ?: return
|
|
||||||
if (node is Host && node.protocol != Protocol.Folder) {
|
|
||||||
doOKAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowClosed(e: WindowEvent) {
|
|
||||||
tree.setModel(null)
|
|
||||||
Database.getDatabase().properties.putString(
|
|
||||||
"HostTreeDialog.HostTreeExpansionState",
|
|
||||||
TreeUtils.saveExpansionState(tree)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
|
||||||
val scrollPane = JScrollPane(tree)
|
|
||||||
scrollPane.border = BorderFactory.createCompoundBorder(
|
|
||||||
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
|
||||||
BorderFactory.createEmptyBorder(4, 6, 4, 6)
|
|
||||||
)
|
|
||||||
|
|
||||||
return scrollPane
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doOKAction() {
|
|
||||||
|
|
||||||
if (allowMulti) {
|
|
||||||
val nodes = tree.getSelectionNodes().filter { it.protocol == Protocol.SSH }
|
|
||||||
if (nodes.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hosts.clear()
|
|
||||||
hosts.addAll(nodes)
|
|
||||||
} else {
|
|
||||||
val node = tree.lastSelectedPathComponent ?: return
|
|
||||||
if (node !is Host || node.protocol != Protocol.SSH) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hosts.clear()
|
|
||||||
hosts.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
super.doOKAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doCancelAction() {
|
|
||||||
hosts.clear()
|
|
||||||
super.doCancelAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import javax.swing.event.TreeModelEvent
|
|
||||||
import javax.swing.event.TreeModelListener
|
|
||||||
import javax.swing.tree.TreeModel
|
|
||||||
import javax.swing.tree.TreePath
|
|
||||||
|
|
||||||
class HostTreeModel : TreeModel {
|
|
||||||
|
|
||||||
val listeners = mutableListOf<TreeModelListener>()
|
|
||||||
|
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
private val hosts = mutableMapOf<String, Host>()
|
|
||||||
private val myRoot by lazy {
|
|
||||||
Host(
|
|
||||||
id = "0",
|
|
||||||
protocol = Protocol.Folder,
|
|
||||||
name = I18n.getString("termora.welcome.my-hosts"),
|
|
||||||
host = StringUtils.EMPTY,
|
|
||||||
port = 0,
|
|
||||||
remark = StringUtils.EMPTY,
|
|
||||||
username = StringUtils.EMPTY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
for (host in hostManager.hosts()) {
|
|
||||||
hosts[host.id] = host
|
|
||||||
}
|
|
||||||
|
|
||||||
hostManager.addHostListener(object : HostListener {
|
|
||||||
override fun hostRemoved(id: String) {
|
|
||||||
val host = hosts[id] ?: return
|
|
||||||
removeNodeFromParent(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hostAdded(host: Host) {
|
|
||||||
// 如果已经存在,那么是修改
|
|
||||||
if (hosts.containsKey(host.id)) {
|
|
||||||
val oldHost = hosts.getValue(host.id)
|
|
||||||
// 父级结构变了
|
|
||||||
if (oldHost.parentId != host.parentId) {
|
|
||||||
hostRemoved(host.id)
|
|
||||||
hostAdded(host)
|
|
||||||
} else {
|
|
||||||
hosts[host.id] = host
|
|
||||||
val event = TreeModelEvent(this, getPathToRoot(host))
|
|
||||||
listeners.forEach { it.treeStructureChanged(event) }
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
hosts[host.id] = host
|
|
||||||
val parent = getParent(host) ?: return
|
|
||||||
val path = TreePath(getPathToRoot(parent))
|
|
||||||
val event = TreeModelEvent(this, path, intArrayOf(getIndexOfChild(parent, host)), arrayOf(host))
|
|
||||||
listeners.forEach { it.treeNodesInserted(event) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hostsChanged() {
|
|
||||||
hosts.clear()
|
|
||||||
for (host in hostManager.hosts()) {
|
|
||||||
hosts[host.id] = host
|
|
||||||
}
|
|
||||||
val event = TreeModelEvent(this, getPathToRoot(root), null, null)
|
|
||||||
listeners.forEach { it.treeStructureChanged(event) }
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRoot(): Host {
|
|
||||||
return myRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChild(parent: Any?, index: Int): Any {
|
|
||||||
return getChildren(parent)[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChildCount(parent: Any?): Int {
|
|
||||||
return getChildren(parent).size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isLeaf(node: Any?): Boolean {
|
|
||||||
return getChildCount(node) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getParent(node: Host): Host? {
|
|
||||||
if (node.parentId == root.id || root.id == node.id) {
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
return hosts.values.firstOrNull { it.id == node.parentId }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
|
||||||
return getChildren(parent).indexOf(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTreeModelListener(listener: TreeModelListener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeTreeModelListener(listener: TreeModelListener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 仅从结构中删除
|
|
||||||
*/
|
|
||||||
fun removeNodeFromParent(host: Host) {
|
|
||||||
val parent = getParent(host) ?: return
|
|
||||||
val index = getIndexOfChild(parent, host)
|
|
||||||
val event = TreeModelEvent(this, TreePath(getPathToRoot(parent)), intArrayOf(index), arrayOf(host))
|
|
||||||
hosts.remove(host.id)
|
|
||||||
listeners.forEach { it.treeNodesRemoved(event) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun visit(host: Host, visitor: (host: Host) -> Unit) {
|
|
||||||
if (host.protocol == Protocol.Folder) {
|
|
||||||
getChildren(host).forEach { visit(it, visitor) }
|
|
||||||
visitor.invoke(host)
|
|
||||||
} else {
|
|
||||||
visitor.invoke(host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHost(id: String): Host? {
|
|
||||||
return hosts[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPathToRoot(host: Host): Array<Host> {
|
|
||||||
|
|
||||||
if (host.id == root.id) {
|
|
||||||
return arrayOf(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
val parents = mutableListOf(host)
|
|
||||||
var pId = host.parentId
|
|
||||||
while (pId != root.id) {
|
|
||||||
val e = hosts[(pId)] ?: break
|
|
||||||
parents.addFirst(e)
|
|
||||||
pId = e.parentId
|
|
||||||
}
|
|
||||||
parents.addFirst(root)
|
|
||||||
return parents.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChildren(parent: Any?): List<Host> {
|
|
||||||
val pId = if (parent is Host) parent.id else root.id
|
|
||||||
return hosts.values.filter { it.parentId == pId }
|
|
||||||
.sortedWith(compareBy<Host> { if (it.protocol == Protocol.Folder) 0 else 1 }.thenBy { it.sort })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
110
src/main/kotlin/app/termora/HostTreeNode.kt
Normal file
110
src/main/kotlin/app/termora/HostTreeNode.kt
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||||
|
import com.formdev.flatlaf.icons.FlatTreeOpenIcon
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.tree.TreeNode
|
||||||
|
|
||||||
|
class HostTreeNode(host: Host) : SimpleTreeNode<Host>(host) {
|
||||||
|
companion object {
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
var host: Host
|
||||||
|
get() = data
|
||||||
|
set(value) = setUserObject(value)
|
||||||
|
|
||||||
|
override val isFolder: Boolean
|
||||||
|
get() = data.protocol == Protocol.Folder
|
||||||
|
|
||||||
|
override val id: String
|
||||||
|
get() = data.id
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的
|
||||||
|
*/
|
||||||
|
override var data: Host
|
||||||
|
get() {
|
||||||
|
val cacheHost = hostManager.getHost((userObject as Host).id)
|
||||||
|
val myHost = userObject as Host
|
||||||
|
if (cacheHost == null) {
|
||||||
|
return myHost
|
||||||
|
}
|
||||||
|
return if (cacheHost.updateDate > myHost.updateDate) cacheHost else myHost
|
||||||
|
}
|
||||||
|
set(value) = setUserObject(value)
|
||||||
|
|
||||||
|
override val folderCount
|
||||||
|
get() = children().toList().count { if (it is HostTreeNode) it.data.protocol == Protocol.Folder else false }
|
||||||
|
|
||||||
|
override fun getParent(): HostTreeNode? {
|
||||||
|
return super.getParent() as HostTreeNode?
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllChildren(): List<HostTreeNode> {
|
||||||
|
return super.getAllChildren().filterIsInstance<HostTreeNode>()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon {
|
||||||
|
return when (host.protocol) {
|
||||||
|
Protocol.Folder -> if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||||
|
Protocol.Serial -> if (selected && hasFocus) Icons.plugin.dark else Icons.plugin
|
||||||
|
else -> if (selected && hasFocus) Icons.terminal.dark else Icons.terminal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun childrenNode(): List<HostTreeNode> {
|
||||||
|
return children?.map { it as HostTreeNode } ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度克隆
|
||||||
|
* @param scopes 克隆的范围
|
||||||
|
*/
|
||||||
|
fun clone(scopes: Set<Protocol> = emptySet()): HostTreeNode {
|
||||||
|
val newNode = clone() as HostTreeNode
|
||||||
|
deepClone(newNode, this, scopes)
|
||||||
|
return newNode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set<Protocol> = emptySet()) {
|
||||||
|
for (child in oldNode.childrenNode()) {
|
||||||
|
if (scopes.isNotEmpty() && !scopes.contains(child.data.protocol)) continue
|
||||||
|
val newChildNode = child.clone() as HostTreeNode
|
||||||
|
deepClone(newChildNode, child, scopes)
|
||||||
|
newNode.add(newChildNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clone(): Any {
|
||||||
|
val newNode = HostTreeNode(data)
|
||||||
|
newNode.children = null
|
||||||
|
newNode.parent = null
|
||||||
|
return newNode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isNodeChild(aNode: TreeNode?): Boolean {
|
||||||
|
if (aNode is HostTreeNode) {
|
||||||
|
for (node in childrenNode()) {
|
||||||
|
if (node.data == aNode.data) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.isNodeChild(aNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as HostTreeNode
|
||||||
|
|
||||||
|
return data == other.data
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return data.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,17 @@ 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 closeSmall by lazy { DynamicIcon("icons/closeSmall.svg", "icons/closeSmall_dark.svg") }
|
||||||
|
val closeSmallHovered by lazy { DynamicIcon("icons/closeSmallHovered.svg", "icons/closeSmallHovered_dark.svg") }
|
||||||
val plugin by lazy { DynamicIcon("icons/plugin.svg", "icons/plugin_dark.svg") }
|
val plugin by lazy { DynamicIcon("icons/plugin.svg", "icons/plugin_dark.svg") }
|
||||||
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_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 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 openInNewWindow by lazy { DynamicIcon("icons/openInNewWindow.svg", "icons/openInNewWindow_dark.svg") }
|
||||||
|
val openInToolWindow by lazy { DynamicIcon("icons/openInToolWindow.svg", "icons/openInToolWindow_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 inspectionsEye by lazy { DynamicIcon("icons/inspectionsEye.svg", "icons/inspectionsEye_dark.svg") }
|
||||||
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_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 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") }
|
||||||
@@ -24,7 +29,11 @@ object Icons {
|
|||||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||||
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 locate by lazy { DynamicIcon("icons/locate.svg", "icons/locate_dark.svg") }
|
||||||
|
val percentage by lazy { DynamicIcon("icons/percentage.svg", "icons/percentage_dark.svg") }
|
||||||
|
val text by lazy { DynamicIcon("icons/text.svg", "icons/text_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 warningIntroduction by lazy { DynamicIcon("icons/warningIntroduction.svg", "icons/warningIntroduction_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") }
|
||||||
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
val clusterRole by lazy { DynamicIcon("icons/clusterRole.svg", "icons/clusterRole_dark.svg") }
|
||||||
val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") }
|
val daemonSets by lazy { DynamicIcon("icons/daemonSets.svg", "icons/daemonSets_dark.svg") }
|
||||||
@@ -43,11 +52,13 @@ object Icons {
|
|||||||
val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") }
|
val moreVertical by lazy { DynamicIcon("icons/moreVertical.svg", "icons/moreVertical_dark.svg") }
|
||||||
val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") }
|
val colors by lazy { DynamicIcon("icons/colors.svg", "icons/colors_dark.svg") }
|
||||||
val chevronDown by lazy { DynamicIcon("icons/chevronDownLarge.svg", "icons/chevronDownLarge_dark.svg") }
|
val chevronDown by lazy { DynamicIcon("icons/chevronDownLarge.svg", "icons/chevronDownLarge_dark.svg") }
|
||||||
|
val chevronRight by lazy { DynamicIcon("icons/chevronRight.svg", "icons/chevronRight_dark.svg") }
|
||||||
val homeFolder by lazy { DynamicIcon("icons/homeFolder.svg", "icons/homeFolder_dark.svg") }
|
val homeFolder by lazy { DynamicIcon("icons/homeFolder.svg", "icons/homeFolder_dark.svg") }
|
||||||
val openNewTab by lazy { DynamicIcon("icons/openNewTab.svg", "icons/openNewTab_dark.svg") }
|
val openNewTab by lazy { DynamicIcon("icons/openNewTab.svg", "icons/openNewTab_dark.svg") }
|
||||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||||
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||||
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||||
|
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }
|
||||||
val azure by lazy { DynamicIcon("icons/azure.svg", "icons/azure_dark.svg") }
|
val azure by lazy { DynamicIcon("icons/azure.svg", "icons/azure_dark.svg") }
|
||||||
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
val revert by lazy { DynamicIcon("icons/revert.svg", "icons/revert_dark.svg") }
|
||||||
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
val edit by lazy { DynamicIcon("icons/edit.svg", "icons/edit_dark.svg") }
|
||||||
@@ -68,6 +79,7 @@ object Icons {
|
|||||||
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }
|
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }
|
||||||
val server by lazy { DynamicIcon("icons/server.svg", "icons/server_dark.svg") }
|
val server by lazy { DynamicIcon("icons/server.svg", "icons/server_dark.svg") }
|
||||||
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
val runAnything by lazy { DynamicIcon("icons/runAnything.svg", "icons/runAnything_dark.svg") }
|
||||||
|
val run by lazy { DynamicIcon("icons/run.svg", "icons/run_dark.svg") }
|
||||||
val uiForm by lazy { DynamicIcon("icons/uiForm.svg", "icons/uiForm_dark.svg") }
|
val uiForm by lazy { DynamicIcon("icons/uiForm.svg", "icons/uiForm_dark.svg") }
|
||||||
val cloud by lazy { DynamicIcon("icons/cloud.svg", "icons/cloud_dark.svg") }
|
val cloud by lazy { DynamicIcon("icons/cloud.svg", "icons/cloud_dark.svg") }
|
||||||
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
val externalLink by lazy { DynamicIcon("icons/externalLink.svg", "icons/externalLink_dark.svg") }
|
||||||
@@ -85,6 +97,9 @@ object Icons {
|
|||||||
val left by lazy { DynamicIcon("icons/left.svg", "icons/left_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 right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") }
|
||||||
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
|
val dotListFiles by lazy { DynamicIcon("icons/dotListFiles.svg", "icons/dotListFiles_dark.svg") }
|
||||||
|
val anyType by lazy { DynamicIcon("icons/anyType.svg", "icons/anyType_dark.svg") }
|
||||||
|
val toolWindowJsonPath by lazy { DynamicIcon("icons/toolWindowJsonPath.svg", "icons/toolWindowJsonPath_dark.svg") }
|
||||||
|
val codeSpan by lazy { DynamicIcon("icons/codeSpan.svg", "icons/codeSpan_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 {
|
val applyNotConflictsLeft by lazy {
|
||||||
@@ -110,5 +125,6 @@ object Icons {
|
|||||||
val listKey by lazy { DynamicIcon("icons/listKey.svg", "icons/listKey_dark.svg") }
|
val listKey by lazy { DynamicIcon("icons/listKey.svg", "icons/listKey_dark.svg") }
|
||||||
val forwardPorts by lazy { DynamicIcon("icons/forwardPorts.svg", "icons/forwardPorts_dark.svg") }
|
val forwardPorts by lazy { DynamicIcon("icons/forwardPorts.svg", "icons/forwardPorts_dark.svg") }
|
||||||
val showWriteAccess by lazy { DynamicIcon("icons/showWriteAccess.svg", "icons/showWriteAccess_dark.svg") }
|
val showWriteAccess by lazy { DynamicIcon("icons/showWriteAccess.svg", "icons/showWriteAccess_dark.svg") }
|
||||||
|
val nvidia by lazy { DynamicIcon("icons/nvidia.svg", "icons/nvidia_dark.svg") }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import java.awt.Window
|
|
||||||
import java.awt.event.KeyAdapter
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import javax.swing.BorderFactory
|
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.UIManager
|
|
||||||
|
|
||||||
class InputDialog(
|
|
||||||
owner: Window,
|
|
||||||
title: String,
|
|
||||||
text: String = StringUtils.EMPTY,
|
|
||||||
placeholderText: String = StringUtils.EMPTY
|
|
||||||
) : DialogWrapper(owner) {
|
|
||||||
private val textField = FlatTextField()
|
|
||||||
private var text: String? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
setSize(340, 60)
|
|
||||||
setLocationRelativeTo(owner)
|
|
||||||
|
|
||||||
super.setTitle(title)
|
|
||||||
|
|
||||||
isResizable = false
|
|
||||||
isModal = true
|
|
||||||
controlsVisible = false
|
|
||||||
titleBarHeight = UIManager.getInt("TabbedPane.tabHeight") * 0.8f
|
|
||||||
|
|
||||||
|
|
||||||
textField.placeholderText = placeholderText
|
|
||||||
textField.text = text
|
|
||||||
textField.addKeyListener(object : KeyAdapter() {
|
|
||||||
override fun keyPressed(e: KeyEvent) {
|
|
||||||
if (e.keyCode == KeyEvent.VK_ENTER) {
|
|
||||||
if (textField.text.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
doOKAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
|
||||||
textField.background = UIManager.getColor("window")
|
|
||||||
textField.border = BorderFactory.createEmptyBorder(0, 13, 0, 13)
|
|
||||||
|
|
||||||
return textField
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getText(): String? {
|
|
||||||
isVisible = true
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doCancelAction() {
|
|
||||||
text = null
|
|
||||||
super.doCancelAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doOKAction() {
|
|
||||||
text = textField.text
|
|
||||||
super.doOKAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createSouthPanel(): JComponent? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,12 @@ 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(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, 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.getInstance(windowScope).createPtyConnector(
|
val ptyConnector = PtyConnectorFactory.getInstance().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,109 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
|
||||||
import com.jetbrains.JBR
|
|
||||||
import com.jetbrains.WindowDecorations.CustomTitleBar
|
|
||||||
import java.awt.Rectangle
|
|
||||||
import java.awt.Window
|
|
||||||
import javax.swing.RootPaneContainer
|
|
||||||
|
|
||||||
class LogicCustomTitleBar(private val titleBar: CustomTitleBar) : CustomTitleBar {
|
|
||||||
companion object {
|
|
||||||
fun createCustomTitleBar(rootPaneContainer: RootPaneContainer): CustomTitleBar {
|
|
||||||
if (!JBR.isWindowDecorationsSupported()) {
|
|
||||||
return LogicCustomTitleBar(object : CustomTitleBar {
|
|
||||||
override fun getHeight(): Float {
|
|
||||||
val bounds = rootPaneContainer.rootPane
|
|
||||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
|
||||||
if (bounds is Rectangle) {
|
|
||||||
return bounds.height.toFloat()
|
|
||||||
}
|
|
||||||
return 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setHeight(height: Float) {
|
|
||||||
rootPaneContainer.rootPane.putClientProperty(
|
|
||||||
FlatClientProperties.TITLE_BAR_HEIGHT,
|
|
||||||
height.toInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getProperties(): MutableMap<String, Any> {
|
|
||||||
return mutableMapOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperties(m: MutableMap<String, *>?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperty(key: String?, value: Any?) {
|
|
||||||
if (key == "controls.visible" && value is Boolean) {
|
|
||||||
rootPaneContainer.rootPane.putClientProperty(
|
|
||||||
FlatClientProperties.TITLE_BAR_SHOW_CLOSE,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLeftInset(): Float {
|
|
||||||
return 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRightInset(): Float {
|
|
||||||
val bounds = rootPaneContainer.rootPane
|
|
||||||
.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
|
||||||
if (bounds is Rectangle) {
|
|
||||||
return bounds.width.toFloat()
|
|
||||||
}
|
|
||||||
return 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun forceHitTest(client: Boolean) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContainingWindow(): Window {
|
|
||||||
return rootPaneContainer as Window
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return JBR.getWindowDecorations().createCustomTitleBar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getHeight(): Float {
|
|
||||||
return titleBar.height
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setHeight(height: Float) {
|
|
||||||
titleBar.height = height
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getProperties(): MutableMap<String, Any> {
|
|
||||||
return titleBar.properties
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperties(m: MutableMap<String, *>?) {
|
|
||||||
titleBar.putProperties(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putProperty(key: String?, value: Any?) {
|
|
||||||
titleBar.putProperty(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLeftInset(): Float {
|
|
||||||
return titleBar.leftInset
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRightInset(): Float {
|
|
||||||
return titleBar.rightInset
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun forceHitTest(client: Boolean) {
|
|
||||||
titleBar.forceHitTest(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContainingWindow(): Window {
|
|
||||||
return titleBar.containingWindow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +1,6 @@
|
|||||||
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 签名和公证问题,依赖二进制依赖会单独在一个文件夹
|
ApplicationInitializr().run()
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
|
||||||
setupNativeLibraries()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
val jSerialComm = FileUtils.getFile(dylib, "jSerialComm")
|
|
||||||
if (jSerialComm.exists()) {
|
|
||||||
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
|
|
||||||
import app.termora.terminal.PtyConnector
|
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当开启转发时,会获取到所有的 [PtyConnector] 然后跳过中间层,直接找到最近的一个 [MultiplePtyConnector],如果找不到那就以最后一个匹配不到的为准 [getMultiplePtyConnector]。
|
|
||||||
*/
|
|
||||||
class MultiplePtyConnector(
|
|
||||||
private val myConnector: PtyConnector
|
|
||||||
) : PtyConnectorDelegate(myConnector) {
|
|
||||||
|
|
||||||
private val isMultiple get() = ActionManager.getInstance().isSelected(Actions.MULTIPLE)
|
|
||||||
private val ptyConnectors
|
|
||||||
get() = ApplicationScope.forApplicationScope()
|
|
||||||
.windowScopes().map { PtyConnectorFactory.getInstance(it).getPtyConnectors() }
|
|
||||||
.flatten()
|
|
||||||
|
|
||||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
|
||||||
if (isMultiple) {
|
|
||||||
for (connector in ptyConnectors) {
|
|
||||||
getMultiplePtyConnector(connector).write(buffer, offset, len)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
myConnector.write(buffer, offset, len)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getMultiplePtyConnector(connector: PtyConnector): PtyConnector {
|
|
||||||
if (connector is MultiplePtyConnector) {
|
|
||||||
val c = connector.myConnector
|
|
||||||
if (c is MultiplePtyConnector) {
|
|
||||||
return getMultiplePtyConnector(c)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connector is PtyConnectorDelegate) {
|
|
||||||
val c = connector.ptyConnector
|
|
||||||
if (c != null) {
|
|
||||||
return getMultiplePtyConnector(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return connector
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
import app.termora.actions.ActionManager
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
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.FloatingToolbarPanel
|
||||||
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.apache.commons.lang3.StringUtils
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class MultipleTerminalListener : TerminalPaintListener {
|
class MultipleTerminalListener : TerminalPaintListener {
|
||||||
override fun after(
|
override fun after(
|
||||||
@@ -20,9 +25,9 @@ class MultipleTerminalListener : TerminalPaintListener {
|
|||||||
terminalDisplay: TerminalDisplay,
|
terminalDisplay: TerminalDisplay,
|
||||||
terminal: Terminal
|
terminal: Terminal
|
||||||
) {
|
) {
|
||||||
if (!ActionManager.getInstance().isSelected(Actions.MULTIPLE)) {
|
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
|
||||||
return
|
.getData(DataProviders.WindowScope) ?: return
|
||||||
}
|
if (!MultipleAction.getInstance(windowScope).isSelected) return
|
||||||
|
|
||||||
val oldFont = g.font
|
val oldFont = g.font
|
||||||
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||||
@@ -32,13 +37,25 @@ class MultipleTerminalListener : TerminalPaintListener {
|
|||||||
// 正在搜索那么需要下移
|
// 正在搜索那么需要下移
|
||||||
val finding = terminal.getTerminalModel().getData(TerminalPanel.Finding, false)
|
val finding = terminal.getTerminalModel().getData(TerminalPanel.Finding, false)
|
||||||
|
|
||||||
|
// 如果悬浮窗正在显示,那么需要下移
|
||||||
|
val floatingToolBar = terminalPanel.getData(FloatingToolbarPanel.FloatingToolbar)?.isVisible == true
|
||||||
|
|
||||||
|
var y = g.fontMetrics.ascent
|
||||||
|
if (finding) {
|
||||||
|
y += g.fontMetrics.height + g.fontMetrics.ascent / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floatingToolBar) {
|
||||||
|
y += g.fontMetrics.height + g.fontMetrics.ascent / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
g.font = font
|
g.font = font
|
||||||
g.color = Color(colorPalette.getColor(TerminalColor.Normal.RED))
|
g.color = Color(colorPalette.getColor(TerminalColor.Normal.RED))
|
||||||
g.drawString(
|
g.drawString(
|
||||||
text,
|
text,
|
||||||
terminalDisplay.width - width - terminalPanel.getAverageCharWidth() / 2,
|
terminalDisplay.width - width - terminalPanel.getAverageCharWidth() / 2,
|
||||||
g.fontMetrics.ascent + if (finding)
|
y
|
||||||
g.fontMetrics.height + g.fontMetrics.ascent / 2 else 0
|
|
||||||
)
|
)
|
||||||
g.font = oldFont
|
g.font = oldFont
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main/kotlin/app/termora/MyFlatRootPaneUI.kt
Normal file
11
src/main/kotlin/app/termora/MyFlatRootPaneUI.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.ui.FlatRootPaneUI
|
||||||
|
import com.formdev.flatlaf.ui.FlatTitlePane
|
||||||
|
|
||||||
|
class MyFlatRootPaneUI : FlatRootPaneUI() {
|
||||||
|
|
||||||
|
fun getTitlePane(): FlatTitlePane? {
|
||||||
|
return super.titlePane
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,20 +9,30 @@ import java.awt.event.*
|
|||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.plaf.TabbedPaneUI
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class MyTabbedPane : FlatTabbedPane() {
|
class MyTabbedPane : FlatTabbedPane() {
|
||||||
|
|
||||||
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
|
||||||
private val dragMouseAdaptor = DragMouseAdaptor()
|
private val dragMouseAdaptor = DragMouseAdaptor()
|
||||||
private val terminalTabbedManager
|
private val terminalTabbedManager
|
||||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
.getData(DataProviders.TerminalTabbedManager)
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
private val owner
|
||||||
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
|
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||||
|
private val myUI = MyFlatTabbedPaneUI()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
isFocusable = false
|
||||||
|
super.setUI(myUI)
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setUI(ui: TabbedPaneUI?) {
|
||||||
|
super.setUI(myUI)
|
||||||
|
}
|
||||||
|
|
||||||
override fun updateUI() {
|
override fun updateUI() {
|
||||||
styleMap = mapOf(
|
styleMap = mapOf(
|
||||||
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
"focusColor" to UIManager.getColor("TabbedPane.selectedBackground"),
|
||||||
@@ -75,10 +85,13 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
private var terminalTab: TerminalTab? = null
|
private var terminalTab: TerminalTab? = null
|
||||||
private var isDragging = false
|
private var isDragging = false
|
||||||
private var lastVisitTabIndex = -1
|
private var lastVisitTabIndex = -1
|
||||||
|
private var releasedPoint = Point()
|
||||||
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
override fun mousePressed(e: MouseEvent) {
|
||||||
val index = indexAtLocation(e.x, e.y)
|
val index = indexAtLocation(e.x, e.y)
|
||||||
if (index < 0 || !isTabClosable(index)) {
|
if (index < 0 || !isTabClosable(index)) {
|
||||||
|
tabIndex = -1
|
||||||
|
mousePressedPoint = Point()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tabIndex = index
|
tabIndex = index
|
||||||
@@ -137,9 +150,16 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是取消,那么不需要移动到其它窗口
|
// 如果是取消,那么不需要移动到其它窗口
|
||||||
val c = if (cancelled) null else getTopMostWindowUnderMouse()
|
val c = if (cancelled) owner else getTopMostWindowUnderMouse()
|
||||||
if (c != owner && c is TermoraFrame) {
|
|
||||||
dragToAnotherWindow(c)
|
// 如果等于 null 表示在空地方释放,那么单独一个窗口
|
||||||
|
if (c == null) {
|
||||||
|
val window = TermoraFrameManager.getInstance().createWindow()
|
||||||
|
dragToAnotherWindow(owner, window)
|
||||||
|
window.location = releasedPoint
|
||||||
|
window.isVisible = true
|
||||||
|
} else if (c != owner && c is TermoraFrame) { // 如果在某个窗口内释放,那么就移动到某个窗口内
|
||||||
|
dragToAnotherWindow(owner, c)
|
||||||
} else {
|
} else {
|
||||||
val tab = this.terminalTab
|
val tab = this.terminalTab
|
||||||
val terminalTabbedManager = terminalTabbedManager
|
val terminalTabbedManager = terminalTabbedManager
|
||||||
@@ -161,6 +181,7 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun mouseReleased(e: MouseEvent) {
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
releasedPoint = e.point
|
||||||
stopDrag()
|
stopDrag()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +234,7 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun dragToAnotherWindow(frame: TermoraFrame) {
|
private fun dragToAnotherWindow(oldFrame: TermoraFrame, frame: TermoraFrame) {
|
||||||
val tab = this.terminalTab ?: return
|
val tab = this.terminalTab ?: return
|
||||||
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
val tabbedManager = frame.getData(DataProviders.TerminalTabbed) ?: return
|
||||||
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
val tabbedPane = frame.getData(DataProviders.TabbedPane) ?: return
|
||||||
@@ -221,6 +242,7 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
SwingUtilities.convertPointFromScreen(location, tabbedPane)
|
||||||
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
val index = tabbedPane.indexAtLocation(location.x, location.y)
|
||||||
|
|
||||||
|
|
||||||
moveTab(
|
moveTab(
|
||||||
tabbedManager,
|
tabbedManager,
|
||||||
tab,
|
tab,
|
||||||
|
|||||||
41
src/main/kotlin/app/termora/NativeStringComparator.kt
Normal file
41
src/main/kotlin/app/termora/NativeStringComparator.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import de.jangassen.jfa.foundation.Foundation
|
||||||
|
import de.jangassen.jfa.foundation.Foundation.NSAutoreleasePool
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class NativeStringComparator private constructor() : Comparator<String> {
|
||||||
|
private val collator by lazy { Collator.getInstance(Locale.getDefault()).apply { strength = Collator.PRIMARY } }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): NativeStringComparator {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(NativeStringComparator::class) { NativeStringComparator() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SORT_DIGITSASNUMBERS: Int = 0x00000008
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compare(o1: String, o2: String): Int {
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
// CompareStringEx returns 1, 2, 3 respectively instead of -1, 0, 1
|
||||||
|
return MyKernel32.INSTANCE.CompareStringEx(SORT_DIGITSASNUMBERS, o1, o2) - 2
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
val pool = NSAutoreleasePool()
|
||||||
|
try {
|
||||||
|
val a = Foundation.nsString(o1)
|
||||||
|
val b = Foundation.nsString(o2)
|
||||||
|
return Foundation.invoke(a, "localizedStandardCompare:", b).toInt()
|
||||||
|
} finally {
|
||||||
|
pool.drain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collator.compare(o1, o2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
902
src/main/kotlin/app/termora/NewHostTree.kt
Normal file
902
src/main/kotlin/app/termora/NewHostTree.kt
Normal file
@@ -0,0 +1,902 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.actions.OpenHostAction
|
||||||
|
import app.termora.sftp.SFTPActionEvent
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import org.apache.commons.csv.CSVFormat
|
||||||
|
import org.apache.commons.csv.CSVParser
|
||||||
|
import org.apache.commons.csv.CSVPrinter
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.apache.commons.io.filefilter.FileFilterUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||||
|
import org.ini4j.Ini
|
||||||
|
import org.ini4j.Reg
|
||||||
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.NodeList
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.event.*
|
||||||
|
import java.io.*
|
||||||
|
import java.util.*
|
||||||
|
import java.util.function.Function
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter
|
||||||
|
import javax.swing.tree.TreePath
|
||||||
|
import javax.swing.tree.TreeSelectionModel
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
import javax.xml.xpath.XPathConstants
|
||||||
|
import javax.xml.xpath.XPathFactory
|
||||||
|
|
||||||
|
class NewHostTree : SimpleTree() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(NewHostTree::class.java)
|
||||||
|
private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private val openHostAction get() = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
private val sftpAction get() = ActionManager.getInstance().getAction(app.termora.Actions.SFTP)
|
||||||
|
private var isShowMoreInfo
|
||||||
|
get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||||
|
set(value) = properties.putString("HostTree.showMoreInfo", value.toString())
|
||||||
|
|
||||||
|
override val model = NewHostTreeModel()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许显示右键菜单
|
||||||
|
*/
|
||||||
|
var contextmenu = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许双击连接
|
||||||
|
*/
|
||||||
|
var doubleClickConnection = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
initViews()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initViews() {
|
||||||
|
super.setModel(model)
|
||||||
|
isEditable = true
|
||||||
|
dragEnabled = true
|
||||||
|
isRootVisible = true
|
||||||
|
dropMode = DropMode.ON_OR_INSERT
|
||||||
|
selectionModel.selectionMode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION
|
||||||
|
|
||||||
|
// renderer
|
||||||
|
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||||
|
override fun getTreeCellRendererComponent(
|
||||||
|
tree: JTree,
|
||||||
|
value: Any,
|
||||||
|
sel: Boolean,
|
||||||
|
expanded: Boolean,
|
||||||
|
leaf: Boolean,
|
||||||
|
row: Int,
|
||||||
|
hasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
val node = value as HostTreeNode
|
||||||
|
val host = node.host
|
||||||
|
var text = host.name
|
||||||
|
|
||||||
|
// 是否显示更多信息
|
||||||
|
if (isShowMoreInfo) {
|
||||||
|
val color = if (sel) {
|
||||||
|
if (tree.hasFocus()) {
|
||||||
|
UIManager.getColor("textHighlightText")
|
||||||
|
} else {
|
||||||
|
this.foreground
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UIManager.getColor("textInactiveText")
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontTag = Function<String, String> {
|
||||||
|
"""<font color=rgb(${color.red},${color.green},${color.blue})>${it}</font>"""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
text =
|
||||||
|
"<html>${host.name} ${fontTag.apply("${host.username}@${host.host}")}</html>"
|
||||||
|
} else if (host.protocol == Protocol.Serial) {
|
||||||
|
text =
|
||||||
|
"<html>${host.name} ${fontTag.apply(host.options.serialComm.port)}</html>"
|
||||||
|
} else if (host.protocol == Protocol.Folder) {
|
||||||
|
text = "<html>${host.name}${fontTag.apply(" (${node.childCount})")}</html>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, hasFocus)
|
||||||
|
|
||||||
|
icon = node.getIcon(sel, expanded, hasFocus)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
// double click
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
if (doubleClickConnection && SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
|
val lastNode = lastSelectedPathComponent as? HostTreeNode ?: return
|
||||||
|
if (lastNode.host.protocol != Protocol.Folder) {
|
||||||
|
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, lastNode.host, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addKeyListener(object : KeyAdapter() {
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
|
||||||
|
val nodes = getSelectionSimpleTreeNodes()
|
||||||
|
if (nodes.size == 1 && nodes.first().host.protocol == Protocol.Folder) {
|
||||||
|
val path = TreePath(model.getPathToRoot(nodes.first()))
|
||||||
|
if (isExpanded(path)) {
|
||||||
|
collapsePath(path)
|
||||||
|
} else {
|
||||||
|
expandPath(path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (node in getSelectionSimpleTreeNodes(true)) {
|
||||||
|
openHostAction?.actionPerformed(OpenHostActionEvent(e.source, node.host, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun showContextmenu(evt: MouseEvent) {
|
||||||
|
if (!contextmenu) return
|
||||||
|
val lastNode = lastSelectedPathComponent
|
||||||
|
if (lastNode !is HostTreeNode) return
|
||||||
|
|
||||||
|
val nodes = getSelectionSimpleTreeNodes()
|
||||||
|
val fullNodes = getSelectionSimpleTreeNodes(true)
|
||||||
|
val lastNodeParent = lastNode.parent ?: model.root
|
||||||
|
val lastHost = lastNode.host
|
||||||
|
|
||||||
|
val popupMenu = FlatPopupMenu()
|
||||||
|
val newMenu = JMenu(I18n.getString("termora.welcome.contextmenu.new"))
|
||||||
|
val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder"))
|
||||||
|
val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host"))
|
||||||
|
val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import"))
|
||||||
|
val csvMenu = importMenu.add("CSV")
|
||||||
|
val xShellMenu = importMenu.add("Xshell")
|
||||||
|
val puTTYMenu = importMenu.add("PuTTY")
|
||||||
|
val electermMenu = importMenu.add("electerm")
|
||||||
|
val finalShellMenu = importMenu.add("FinalShell")
|
||||||
|
val windTermMenu = importMenu.add("WindTerm")
|
||||||
|
val secureCRTMenu = importMenu.add("SecureCRT")
|
||||||
|
val sshMenu = importMenu.add(".ssh/config")
|
||||||
|
val mobaXtermMenu = importMenu.add("MobaXterm")
|
||||||
|
|
||||||
|
|
||||||
|
val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||||
|
val openWith = popupMenu.add(JMenu(I18n.getString("termora.welcome.contextmenu.connect-with"))) as JMenu
|
||||||
|
val openWithSFTP = openWith.add("SFTP")
|
||||||
|
val openWithSFTPCommand = openWith.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
val openInNewWindow = popupMenu.add(I18n.getString("termora.welcome.contextmenu.open-in-new-window"))
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val copy = popupMenu.add(I18n.getString("termora.welcome.contextmenu.copy"))
|
||||||
|
val remove = popupMenu.add(I18n.getString("termora.welcome.contextmenu.remove"))
|
||||||
|
val rename = popupMenu.add(I18n.getString("termora.welcome.contextmenu.rename"))
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val refresh = popupMenu.add(I18n.getString("termora.welcome.contextmenu.refresh"))
|
||||||
|
val expandAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.expand-all"))
|
||||||
|
val colspanAll = popupMenu.add(I18n.getString("termora.welcome.contextmenu.collapse-all"))
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
popupMenu.add(importMenu)
|
||||||
|
popupMenu.add(newMenu)
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val showMoreInfo = JCheckBoxMenuItem(I18n.getString("termora.welcome.contextmenu.show-more-info"))
|
||||||
|
showMoreInfo.isSelected = isShowMoreInfo
|
||||||
|
showMoreInfo.addActionListener {
|
||||||
|
isShowMoreInfo = !isShowMoreInfo
|
||||||
|
SwingUtilities.updateComponentTreeUI(tree)
|
||||||
|
}
|
||||||
|
popupMenu.add(showMoreInfo)
|
||||||
|
val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property"))
|
||||||
|
|
||||||
|
xShellMenu.addActionListener { importHosts(lastNode, ImportType.Xshell) }
|
||||||
|
puTTYMenu.addActionListener { importHosts(lastNode, ImportType.PuTTY) }
|
||||||
|
secureCRTMenu.addActionListener { importHosts(lastNode, ImportType.SecureCRT) }
|
||||||
|
electermMenu.addActionListener { importHosts(lastNode, ImportType.electerm) }
|
||||||
|
mobaXtermMenu.addActionListener { importHosts(lastNode, ImportType.MobaXterm) }
|
||||||
|
sshMenu.addActionListener { importHosts(lastNode, ImportType.SSH) }
|
||||||
|
finalShellMenu.addActionListener { importHosts(lastNode, ImportType.FinalShell) }
|
||||||
|
csvMenu.addActionListener { importHosts(lastNode, ImportType.CSV) }
|
||||||
|
windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) }
|
||||||
|
open.addActionListener { openHosts(it, false) }
|
||||||
|
openInNewWindow.addActionListener { openHosts(it, true) }
|
||||||
|
openWithSFTP.addActionListener { openWithSFTP(it) }
|
||||||
|
openWithSFTPCommand.addActionListener { openWithSFTPCommand(it) }
|
||||||
|
newFolder.addActionListener {
|
||||||
|
val host = Host(
|
||||||
|
id = UUID.randomUUID().toSimpleString(),
|
||||||
|
protocol = Protocol.Folder,
|
||||||
|
name = I18n.getString("termora.welcome.contextmenu.new.folder.name"),
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
parentId = lastHost.id
|
||||||
|
)
|
||||||
|
hostManager.addHost(host)
|
||||||
|
val newNode = HostTreeNode(host)
|
||||||
|
model.insertNodeInto(newNode, lastNode, lastNode.folderCount)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
startEditingAtPath(selectionPath)
|
||||||
|
}
|
||||||
|
remove.addActionListener(object : ActionListener {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(tree),
|
||||||
|
I18n.getString("termora.keymgr.delete-warning"),
|
||||||
|
I18n.getString("termora.remove"),
|
||||||
|
JOptionPane.YES_NO_OPTION,
|
||||||
|
JOptionPane.QUESTION_MESSAGE
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
for (c in nodes) {
|
||||||
|
hostManager.addHost(c.host.copy(deleted = true, updateDate = System.currentTimeMillis()))
|
||||||
|
model.removeNodeFromParent(c)
|
||||||
|
// 将所有子孙也删除
|
||||||
|
for (child in c.getAllChildren()) {
|
||||||
|
hostManager.addHost(
|
||||||
|
child.host.copy(
|
||||||
|
deleted = true,
|
||||||
|
updateDate = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
copy.addActionListener {
|
||||||
|
for (c in nodes) {
|
||||||
|
val p = c.parent ?: continue
|
||||||
|
val newNode = copyNode(c, p.host.id)
|
||||||
|
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) }
|
||||||
|
expandAll.addActionListener {
|
||||||
|
for (node in fullNodes) {
|
||||||
|
expandPath(TreePath(model.getPathToRoot(node)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colspanAll.addActionListener {
|
||||||
|
for (node in fullNodes.reversed()) {
|
||||||
|
collapsePath(TreePath(model.getPathToRoot(node)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newHost.addActionListener(object : ActionListener {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = HostDialog(owner)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
||||||
|
hostManager.addHost(host)
|
||||||
|
val newNode = HostTreeNode(host)
|
||||||
|
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
property.addActionListener(object : ActionListener {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val dialog = HostDialog(owner, lastHost)
|
||||||
|
dialog.title = lastHost.name
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
|
dialog.isVisible = true
|
||||||
|
val host = dialog.host ?: return
|
||||||
|
lastNode.host = host
|
||||||
|
hostManager.addHost(host)
|
||||||
|
model.nodeStructureChanged(lastNode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
refresh.addActionListener { refreshNode(lastNode) }
|
||||||
|
|
||||||
|
newMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||||
|
remove.isEnabled = getSelectionSimpleTreeNodes().none { it == model.root }
|
||||||
|
copy.isEnabled = remove.isEnabled
|
||||||
|
rename.isEnabled = remove.isEnabled
|
||||||
|
property.isEnabled = lastHost.protocol != Protocol.Folder
|
||||||
|
refresh.isEnabled = lastHost.protocol == Protocol.Folder
|
||||||
|
importMenu.isEnabled = lastHost.protocol == Protocol.Folder
|
||||||
|
|
||||||
|
// 如果选中了 SSH 服务器,那么才启用
|
||||||
|
openWithSFTP.isEnabled = fullNodes.map { it.host }.any { it.protocol == Protocol.SSH }
|
||||||
|
openWithSFTPCommand.isEnabled = openWithSFTP.isEnabled
|
||||||
|
openWith.isEnabled = openWith.menuComponents.any { it is JMenuItem && it.isEnabled }
|
||||||
|
popupMenu.show(this, evt.x, evt.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
|
||||||
|
val lastNode = node as? HostTreeNode ?: return
|
||||||
|
lastNode.host = lastNode.host.copy(name = text, updateDate = System.currentTimeMillis())
|
||||||
|
model.nodeStructureChanged(lastNode)
|
||||||
|
hostManager.addHost(lastNode.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||||
|
val nNode = node as? HostTreeNode ?: return
|
||||||
|
val nParent = parent as? HostTreeNode ?: return
|
||||||
|
nNode.data = nNode.data.copy(parentId = nParent.id, updateDate = System.currentTimeMillis())
|
||||||
|
hostManager.addHost(nNode.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun copyNode(
|
||||||
|
node: HostTreeNode,
|
||||||
|
parentId: String,
|
||||||
|
idGenerator: () -> String = { UUID.randomUUID().toSimpleString() },
|
||||||
|
level: Int = 0
|
||||||
|
): HostTreeNode {
|
||||||
|
|
||||||
|
val host = node.host
|
||||||
|
val now = host.sort + 1
|
||||||
|
val name = if (level == 0) "${host.name} ${I18n.getString("termora.welcome.contextmenu.copy")}"
|
||||||
|
else host.name
|
||||||
|
|
||||||
|
val newHost = host.copy(
|
||||||
|
id = idGenerator.invoke(),
|
||||||
|
name = name,
|
||||||
|
parentId = parentId,
|
||||||
|
updateDate = System.currentTimeMillis(),
|
||||||
|
createDate = System.currentTimeMillis(),
|
||||||
|
sort = now
|
||||||
|
)
|
||||||
|
val newNode = HostTreeNode(newHost)
|
||||||
|
|
||||||
|
hostManager.addHost(newHost)
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.Folder) {
|
||||||
|
for (child in node.children()) {
|
||||||
|
if (child is HostTreeNode) {
|
||||||
|
newNode.add(copyNode(child, newHost.id, idGenerator, level + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNode
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSelectionSimpleTreeNodes(include: Boolean): List<HostTreeNode> {
|
||||||
|
return super.getSelectionSimpleTreeNodes(include).filterIsInstance<HostTreeNode>()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openHosts(evt: EventObject, openInNewWindow: Boolean) {
|
||||||
|
assertEventDispatchThread()
|
||||||
|
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol != Protocol.Folder }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
val source = if (openInNewWindow)
|
||||||
|
TermoraFrameManager.getInstance().createWindow().apply { isVisible = true }
|
||||||
|
else evt.source
|
||||||
|
nodes.forEach { openHostAction.actionPerformed(OpenHostActionEvent(source, it, evt)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openWithSFTP(evt: EventObject) {
|
||||||
|
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
|
||||||
|
for (node in nodes) {
|
||||||
|
sftpAction.actionPerformed(SFTPActionEvent(this, node.id, evt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openWithSFTPCommand(evt: EventObject) {
|
||||||
|
val nodes = getSelectionSimpleTreeNodes(true).map { it.host }.filter { it.protocol == Protocol.SSH }
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
for (host in nodes) {
|
||||||
|
openHostAction.actionPerformed(OpenHostActionEvent(this, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importHosts(folder: HostTreeNode, type: ImportType) {
|
||||||
|
try {
|
||||||
|
doImportHosts(folder, type)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
OptionPane.showMessageDialog(owner, ExceptionUtils.getMessage(e), messageType = JOptionPane.ERROR_MESSAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doImportHosts(folder: HostTreeNode, type: ImportType) {
|
||||||
|
val chooser = JFileChooser()
|
||||||
|
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
|
chooser.isAcceptAllFileFilterUsed = false
|
||||||
|
chooser.isMultiSelectionEnabled = false
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
ImportType.WindTerm -> chooser.fileFilter = FileNameExtensionFilter("WindTerm (*.sessions)", "sessions")
|
||||||
|
ImportType.SSH -> chooser.fileFilter = FileNameExtensionFilter("SSH (config)", "config")
|
||||||
|
ImportType.CSV -> chooser.fileFilter = FileNameExtensionFilter("CSV (*.csv)", "csv")
|
||||||
|
ImportType.SecureCRT -> chooser.fileFilter = FileNameExtensionFilter("SecureCRT (*.xml)", "xml")
|
||||||
|
ImportType.electerm -> chooser.fileFilter = FileNameExtensionFilter("electerm (*.json)", "json")
|
||||||
|
ImportType.PuTTY -> chooser.fileFilter = FileNameExtensionFilter("PuTTY (*.reg)", "reg")
|
||||||
|
ImportType.MobaXterm -> chooser.fileFilter =
|
||||||
|
FileNameExtensionFilter("MobaXterm (*.mobaconf,*.ini)", "ini", "mobaconf")
|
||||||
|
|
||||||
|
ImportType.Xshell -> {
|
||||||
|
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
|
chooser.dialogTitle = "Xshell Sessions"
|
||||||
|
chooser.isAcceptAllFileFilterUsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportType.FinalShell -> {
|
||||||
|
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
|
chooser.isAcceptAllFileFilterUsed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY)
|
||||||
|
if (dir.isNotBlank()) {
|
||||||
|
val file = FileUtils.getFile(dir)
|
||||||
|
if (file.exists()) {
|
||||||
|
chooser.currentDirectory = file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// csv template
|
||||||
|
if (type == ImportType.CSV) {
|
||||||
|
val code = OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.welcome.contextmenu.import.csv.download-template"),
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||||
|
options = arrayOf(
|
||||||
|
I18n.getString("termora.welcome.contextmenu.import"),
|
||||||
|
I18n.getString("termora.welcome.contextmenu.download")
|
||||||
|
),
|
||||||
|
initialValue = I18n.getString("termora.welcome.contextmenu.import")
|
||||||
|
)
|
||||||
|
if (code == JOptionPane.DEFAULT_OPTION) {
|
||||||
|
return
|
||||||
|
} else if (code != JOptionPane.YES_OPTION) {
|
||||||
|
chooser.setSelectedFile(File("termora_import.csv"))
|
||||||
|
if (chooser.showSaveDialog(owner) == JFileChooser.APPROVE_OPTION) {
|
||||||
|
CSVPrinter(
|
||||||
|
FileWriter(chooser.selectedFile, Charsets.UTF_8),
|
||||||
|
CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()
|
||||||
|
).use { printer ->
|
||||||
|
printer.printRecord("Projects/Dev", "Web Server", "192.168.1.1", "22", "root", "SSH")
|
||||||
|
printer.printRecord("Projects/Prod", "Web Server", "serverhost.com", "2222", "root", "SSH")
|
||||||
|
printer.printRecord(StringUtils.EMPTY, "Web Server", "serverhost.com", "2222", "user", "SSH")
|
||||||
|
}
|
||||||
|
OptionPane.openFileInFolder(
|
||||||
|
owner,
|
||||||
|
chooser.selectedFile,
|
||||||
|
I18n.getString("termora.welcome.contextmenu.import.csv.download-template-done-open-folder"),
|
||||||
|
I18n.getString("termora.welcome.contextmenu.import.csv.download-template-done")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择文件
|
||||||
|
if (type != ImportType.SSH) {
|
||||||
|
val code = chooser.showOpenDialog(owner)
|
||||||
|
if (code != JFileChooser.APPROVE_OPTION) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = chooser.selectedFile
|
||||||
|
if (file != null && file.parentFile != null) {
|
||||||
|
properties.putString(
|
||||||
|
"NewHostTree.ImportHosts.defaultDir",
|
||||||
|
(if (FileUtils.isDirectory(file)) file else file.parentFile).absolutePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val nodes = when (type) {
|
||||||
|
ImportType.SSH -> parseFromSSH(folder)
|
||||||
|
ImportType.WindTerm -> parseFromWindTerm(folder, file)
|
||||||
|
ImportType.SecureCRT -> parseFromSecureCRT(folder, file)
|
||||||
|
ImportType.MobaXterm -> parseFromMobaXterm(folder, file)
|
||||||
|
ImportType.PuTTY -> parseFromPuTTY(folder, file)
|
||||||
|
ImportType.Xshell -> parseFromXshell(folder, file)
|
||||||
|
ImportType.FinalShell -> parseFromFinalShell(folder, file)
|
||||||
|
ImportType.electerm -> parseFromElecterm(folder, file)
|
||||||
|
ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
|
||||||
|
for (node in nodes) {
|
||||||
|
node.host = node.host.copy(parentId = folder.host.id, updateDate = System.currentTimeMillis())
|
||||||
|
if (folder.getIndex(node) != -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
model.insertNodeInto(
|
||||||
|
node,
|
||||||
|
folder,
|
||||||
|
if (node.host.protocol == Protocol.Folder) folder.folderCount else folder.childCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (node in nodes) {
|
||||||
|
hostManager.addHost(node.host)
|
||||||
|
node.getAllChildren().forEach { hostManager.addHost(it.host) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载
|
||||||
|
model.reload(folder)
|
||||||
|
|
||||||
|
// expand root
|
||||||
|
expandPath(TreePath(model.getPathToRoot(folder)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||||
|
val sessions = ohMyJson.runCatching { ohMyJson.parseToJsonElement(file.readText()).jsonArray }
|
||||||
|
.onFailure { OptionPane.showMessageDialog(owner, ExceptionUtils.getMessage(it)) }
|
||||||
|
.getOrNull() ?: return emptyList()
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (i in 0 until sessions.size) {
|
||||||
|
val json = sessions[i].jsonObject
|
||||||
|
val protocol = json["session.protocol"]?.jsonPrimitive?.content ?: "SSH"
|
||||||
|
if (!StringUtils.equalsIgnoreCase("SSH", protocol)) continue
|
||||||
|
val label = json["session.label"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val target = json["session.target"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val port = json["session.port"]?.jsonPrimitive?.intOrNull ?: 22
|
||||||
|
val group = json["session.group"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val groups = group.split(">")
|
||||||
|
printer.printRecord(groups.joinToString("/"), label, target, port, StringUtils.EMPTY, "SSH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromSSH(folder: HostTreeNode): List<HostTreeNode> {
|
||||||
|
val entries = HostConfigEntry.readHostConfigEntries(HostConfigEntry.getDefaultHostConfigFile())
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (entry in entries) {
|
||||||
|
printer.printRecord(
|
||||||
|
StringUtils.EMPTY,
|
||||||
|
StringUtils.defaultString(entry.host),
|
||||||
|
StringUtils.defaultString(entry.hostName),
|
||||||
|
if (entry.port == 0) 22 else entry.port,
|
||||||
|
StringUtils.defaultString(entry.username),
|
||||||
|
"SSH"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromSecureCRT(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||||
|
val xPath = XPathFactory.newInstance().newXPath()
|
||||||
|
val db = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||||
|
val doc = db.parse(file)
|
||||||
|
val sessionElement = xPath.compile("/VanDyke/key[@name='Sessions']")
|
||||||
|
.evaluate(doc, XPathConstants.NODE) as Element? ?: return emptyList()
|
||||||
|
val nodeList = xPath.compile(".//key[not(key)]").evaluate(sessionElement, XPathConstants.NODESET) as NodeList
|
||||||
|
if (nodeList.length == 0) return emptyList()
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (i in 0 until nodeList.length) {
|
||||||
|
val ele = nodeList.item(i) as Element
|
||||||
|
val protocol = xPath.compile("./string[@name='Protocol Name']/text()").evaluate(ele)
|
||||||
|
if (!StringUtils.equalsIgnoreCase(protocol, "SSH2")) continue
|
||||||
|
val label = ele.getAttribute("name")
|
||||||
|
if (StringUtils.isBlank(label)) continue
|
||||||
|
val hostname = xPath.compile("./string[@name='Hostname']/text()").evaluate(ele)
|
||||||
|
if (StringUtils.isBlank(hostname)) continue
|
||||||
|
val username = xPath.compile("./string[@name='Username']/text()").evaluate(ele)
|
||||||
|
val port = xPath.compile("./dword[@name='[SSH2] Port']/text()").evaluate(ele)?.toIntOrNull() ?: 22
|
||||||
|
|
||||||
|
|
||||||
|
val folders = mutableListOf<String>()
|
||||||
|
var p = ele.parentNode as Element
|
||||||
|
while (p != sessionElement) {
|
||||||
|
folders.addFirst(p.getAttribute("name"))
|
||||||
|
p = p.parentNode as Element
|
||||||
|
}
|
||||||
|
printer.printRecord(folders.joinToString("/"), label, hostname, port.toString(), username, "SSH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromPuTTY(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||||
|
val reg = Reg(file)
|
||||||
|
val prefix = "HKEY_CURRENT_USER\\Software\\SimonTatham\\PuTTY\\Sessions\\"
|
||||||
|
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (key in reg.keys) {
|
||||||
|
if (!key.startsWith(prefix)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val properties = reg[key]?.toProperties() ?: continue
|
||||||
|
val label = StringUtils.removeStart(key, prefix)
|
||||||
|
val hostname = properties.getProperty("HostName")
|
||||||
|
val username = properties.getProperty("UserName")
|
||||||
|
val port = properties.getProperty("PortNumber")
|
||||||
|
printer.printRecord(StringUtils.EMPTY, label, hostname, port.toString(), username, "SSH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromMobaXterm(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||||
|
val ini = Ini()
|
||||||
|
ini.config.isEscapeKeyOnly = true
|
||||||
|
ini.load(file)
|
||||||
|
|
||||||
|
val bookmarks = mutableListOf<String>()
|
||||||
|
for (key in ini.keys) {
|
||||||
|
if (key.startsWith("Bookmarks")) {
|
||||||
|
bookmarks.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
|
||||||
|
for (bookmark in bookmarks) {
|
||||||
|
val properties = (ini[bookmark] ?: continue).toProperties()
|
||||||
|
// 删除不必要元素
|
||||||
|
properties.remove("ImgNum")
|
||||||
|
val folders = FilenameUtils.separatorsToUnix(
|
||||||
|
(properties.remove("SubRep")
|
||||||
|
?: StringUtils.EMPTY).toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
for (key in properties.stringPropertyNames()) {
|
||||||
|
val segments = properties.getProperty(key).split("%")
|
||||||
|
if (segments.isEmpty()) continue
|
||||||
|
// ssh: #109#0
|
||||||
|
// telnet: #98#1
|
||||||
|
if (segments.first() != "#109#0") continue
|
||||||
|
val hostname = segments.getOrNull(1) ?: StringUtils.EMPTY
|
||||||
|
val port = segments.getOrNull(2) ?: 22
|
||||||
|
printer.printRecord(folders, key, hostname, port, StringUtils.EMPTY, "SSH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromXshell(folder: HostTreeNode, dir: File): List<HostTreeNode> {
|
||||||
|
val files = FileUtils.listFiles(dir, arrayOf("xsh"), true)
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.welcome.contextmenu.import.xshell-folder-empty")
|
||||||
|
)
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (file in files) {
|
||||||
|
val ini = Ini(file)
|
||||||
|
val protocol = ini.get("CONNECTION", "Protocol") ?: "SSH"
|
||||||
|
if (!StringUtils.equalsIgnoreCase("SSH", protocol)) continue
|
||||||
|
val folders = FilenameUtils.separatorsToUnix(file.parentFile.relativeTo(dir).toString())
|
||||||
|
val hostname = ini.get("CONNECTION", "Host") ?: StringUtils.EMPTY
|
||||||
|
val label = file.nameWithoutExtension
|
||||||
|
val port = ini.get("CONNECTION", "Port")?.toIntOrNull() ?: 22
|
||||||
|
val username = ini.get("CONNECTION:AUTHENTICATION", "UserName") ?: StringUtils.EMPTY
|
||||||
|
printer.printRecord(folders, label, hostname, port, username, "SSH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromFinalShell(folder: HostTreeNode, dir: File): List<HostTreeNode> {
|
||||||
|
val files = FileUtils.listFiles(
|
||||||
|
dir,
|
||||||
|
FileFilterUtils.suffixFileFilter("_connect_config.json"),
|
||||||
|
FileFilterUtils.trueFileFilter()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.welcome.contextmenu.import.finalshell-folder-empty")
|
||||||
|
)
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (file in files) {
|
||||||
|
try {
|
||||||
|
val json = ohMyJson.runCatching { ohMyJson.parseToJsonElement(file.readText()) }
|
||||||
|
.getOrNull()?.jsonObject ?: continue
|
||||||
|
val username = json["user_name"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val label = json["name"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val host = json["host"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val port = json["port"]?.jsonPrimitive?.intOrNull ?: 22
|
||||||
|
if (StringUtils.isAllBlank(host, label)) continue
|
||||||
|
val folders = FilenameUtils.separatorsToUnix(file.parentFile.relativeTo(dir).toString())
|
||||||
|
printer.printRecord(folders, StringUtils.defaultIfBlank(label, host), host, port, username, "SSH")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(file.absolutePath, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class ElectermGroup(
|
||||||
|
val id: String = StringUtils.EMPTY,
|
||||||
|
val title: String = StringUtils.EMPTY,
|
||||||
|
val bookmarkIds: Set<String> = emptySet(),
|
||||||
|
val bookmarkGroupIds: Set<String> = emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseFromElecterm(folder: HostTreeNode, file: File): List<HostTreeNode> {
|
||||||
|
val json = ohMyJson.parseToJsonElement(file.readText()).jsonObject
|
||||||
|
val bookmarks = json["bookmarks"]?.jsonArray ?: return emptyList()
|
||||||
|
val bookmarkGroups = ohMyJson.decodeFromJsonElement<List<ElectermGroup>>(
|
||||||
|
json["bookmarkGroups"]?.jsonArray ?: JsonArray(emptyList())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val sw = StringWriter()
|
||||||
|
CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer ->
|
||||||
|
for (i in 0 until bookmarks.size) {
|
||||||
|
val host = bookmarks[i].jsonObject
|
||||||
|
val type = host["type"]?.jsonPrimitive?.content ?: "SSH"
|
||||||
|
if (!StringUtils.equalsIgnoreCase(type, "SSH")) continue
|
||||||
|
val hostname = host["host"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val id = host["id"]?.jsonPrimitive?.content ?: continue
|
||||||
|
val title = host["title"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
if (StringUtils.isAllBlank(title, hostname)) continue
|
||||||
|
val username = host["username"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
val port = host["port"]?.jsonPrimitive?.intOrNull ?: 22
|
||||||
|
|
||||||
|
val folderNames = mutableListOf<String>()
|
||||||
|
var group = bookmarkGroups.find { it.bookmarkIds.contains(id) }
|
||||||
|
while (group != null && group.id != "default") {
|
||||||
|
folderNames.addFirst(group.title)
|
||||||
|
group = bookmarkGroups.find { it.bookmarkGroupIds.contains(group?.id ?: StringUtils.EMPTY) }
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.printRecord(
|
||||||
|
folderNames.joinToString("/"),
|
||||||
|
StringUtils.defaultIfBlank(title, hostname),
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
"SSH"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFromCSV(folder, StringReader(sw.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFromCSV(folderNode: HostTreeNode, sr: Reader): List<HostTreeNode> {
|
||||||
|
val records = CSVParser.builder()
|
||||||
|
.setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get())
|
||||||
|
.setCharset(Charsets.UTF_8)
|
||||||
|
.setReader(sr)
|
||||||
|
.get()
|
||||||
|
.use { it.records }
|
||||||
|
// 把现有目录提取出来,避免重复创建
|
||||||
|
val nodes = folderNode.clone(setOf(Protocol.Folder))
|
||||||
|
.childrenNode().filter { it.host.protocol == Protocol.Folder }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
for (record in records) {
|
||||||
|
val map = mutableMapOf<String, String>()
|
||||||
|
for (e in record.parser.headerMap.keys) {
|
||||||
|
map[e] = record.get(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val folder = map["Folders"] ?: StringUtils.EMPTY
|
||||||
|
val label = map["Label"] ?: StringUtils.EMPTY
|
||||||
|
val hostname = map["Hostname"] ?: StringUtils.EMPTY
|
||||||
|
val port = map["Port"]?.toIntOrNull() ?: 22
|
||||||
|
val username = map["Username"] ?: StringUtils.EMPTY
|
||||||
|
val protocol = map["Protocol"] ?: "SSH"
|
||||||
|
if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue
|
||||||
|
if (StringUtils.isAllBlank(hostname, label)) continue
|
||||||
|
|
||||||
|
var p: HostTreeNode? = null
|
||||||
|
if (folder.isNotBlank()) {
|
||||||
|
for ((j, name) in folder.split("/").withIndex()) {
|
||||||
|
val folders = if (j == 0 || p == null) nodes
|
||||||
|
else p.children().toList().filterIsInstance<HostTreeNode>()
|
||||||
|
val n = HostTreeNode(
|
||||||
|
Host(
|
||||||
|
name = name, protocol = Protocol.Folder,
|
||||||
|
parentId = p?.host?.id ?: StringUtils.EMPTY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val cp = folders.find { it.host.protocol == Protocol.Folder && it.host.name == name }
|
||||||
|
if (cp != null) {
|
||||||
|
p = cp
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (p == null) {
|
||||||
|
p = n
|
||||||
|
nodes.add(n)
|
||||||
|
} else {
|
||||||
|
p.add(n)
|
||||||
|
p = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val n = HostTreeNode(
|
||||||
|
Host(
|
||||||
|
name = StringUtils.defaultIfBlank(label, hostname),
|
||||||
|
host = hostname,
|
||||||
|
port = port,
|
||||||
|
username = username,
|
||||||
|
protocol = Protocol.SSH,
|
||||||
|
parentId = p?.host?.id ?: StringUtils.EMPTY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (p == null) {
|
||||||
|
nodes.add(n)
|
||||||
|
} else {
|
||||||
|
p.add(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private enum class ImportType {
|
||||||
|
WindTerm,
|
||||||
|
CSV,
|
||||||
|
Xshell,
|
||||||
|
PuTTY,
|
||||||
|
SecureCRT,
|
||||||
|
MobaXterm,
|
||||||
|
SSH,
|
||||||
|
FinalShell,
|
||||||
|
electerm,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
95
src/main/kotlin/app/termora/NewHostTreeDialog.kt
Normal file
95
src/main/kotlin/app/termora/NewHostTreeDialog.kt
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.util.function.Function
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class NewHostTreeDialog(
|
||||||
|
owner: Window,
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
var hosts = emptyList<Host>()
|
||||||
|
var allowMulti = true
|
||||||
|
|
||||||
|
private var filter: Function<HostTreeNode, Boolean> = Function<HostTreeNode, Boolean> { true }
|
||||||
|
private val tree = NewHostTree()
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 250, UIManager.getInt("Dialog.height") - 150)
|
||||||
|
isModal = true
|
||||||
|
isResizable = false
|
||||||
|
controlsVisible = false
|
||||||
|
title = I18n.getString("termora.transport.sftp.select-host")
|
||||||
|
|
||||||
|
tree.contextmenu = false
|
||||||
|
tree.doubleClickConnection = false
|
||||||
|
tree.dragEnabled = false
|
||||||
|
|
||||||
|
tree.addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||||
|
val node = tree.getLastSelectedPathNode() ?: return
|
||||||
|
if (node.isFolder) return
|
||||||
|
doOKAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
init()
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFilter(filter: Function<HostTreeNode, Boolean>) {
|
||||||
|
tree.model = FilterableHostTreeModel(tree) { false }.apply {
|
||||||
|
addFilter(filter)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
val scrollPane = JScrollPane(tree)
|
||||||
|
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(4, 6, 4, 6)
|
||||||
|
)
|
||||||
|
|
||||||
|
return scrollPane
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
hosts = emptyList()
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
hosts = tree.getSelectionSimpleTreeNodes(true)
|
||||||
|
.filter { filter.apply(it) }
|
||||||
|
.map { it.host }
|
||||||
|
|
||||||
|
if (hosts.isEmpty()) return
|
||||||
|
if (!allowMulti && hosts.size > 1) return
|
||||||
|
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTreeName(treeName: String) {
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
private val key = "${treeName}.state"
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
|
||||||
|
init {
|
||||||
|
TreeUtils.loadExpansionState(tree, properties.getString(key, StringUtils.EMPTY))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
properties.putString(key, TreeUtils.saveExpansionState(tree))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/main/kotlin/app/termora/NewHostTreeModel.kt
Normal file
82
src/main/kotlin/app/termora/NewHostTreeModel.kt
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import javax.swing.tree.MutableTreeNode
|
||||||
|
import javax.swing.tree.TreeNode
|
||||||
|
|
||||||
|
|
||||||
|
class NewHostTreeModel : SimpleTreeModel<Host>(
|
||||||
|
HostTreeNode(
|
||||||
|
Host(
|
||||||
|
id = "0",
|
||||||
|
protocol = Protocol.Folder,
|
||||||
|
name = I18n.getString("termora.welcome.my-hosts"),
|
||||||
|
host = StringUtils.EMPTY,
|
||||||
|
port = 0,
|
||||||
|
remark = StringUtils.EMPTY,
|
||||||
|
username = StringUtils.EMPTY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
private val Host.isRoot get() = this.parentId == "0" || this.parentId.isBlank()
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
|
||||||
|
init {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getRoot(): HostTreeNode {
|
||||||
|
return super.getRoot() as HostTreeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun reload(parent: TreeNode) {
|
||||||
|
|
||||||
|
if (parent !is HostTreeNode) {
|
||||||
|
super.reload(parent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.removeAllChildren()
|
||||||
|
|
||||||
|
val hosts = hostManager.hosts()
|
||||||
|
val nodes = linkedMapOf<String, HostTreeNode>()
|
||||||
|
|
||||||
|
// 遍历 Host 列表,构建树节点
|
||||||
|
for (host in hosts) {
|
||||||
|
val node = HostTreeNode(host)
|
||||||
|
nodes[host.id] = node
|
||||||
|
}
|
||||||
|
|
||||||
|
for (host in hosts) {
|
||||||
|
val node = nodes[host.id] ?: continue
|
||||||
|
if (host.isRoot) continue
|
||||||
|
val p = nodes[host.parentId] ?: continue
|
||||||
|
p.add(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((_, v) in nodes.entries) {
|
||||||
|
if (parent.host.id == v.host.parentId) {
|
||||||
|
parent.add(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.reload(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insertNodeInto(newChild: MutableTreeNode, parent: MutableTreeNode, index: Int) {
|
||||||
|
super.insertNodeInto(newChild, parent, index)
|
||||||
|
// 重置所有排序
|
||||||
|
if (parent is HostTreeNode) {
|
||||||
|
for ((i, c) in parent.children().toList().filterIsInstance<HostTreeNode>().withIndex()) {
|
||||||
|
val sort = i.toLong()
|
||||||
|
if (c.host.sort == sort) continue
|
||||||
|
c.host = c.host.copy(sort = sort, updateDate = System.currentTimeMillis())
|
||||||
|
hostManager.addHost(c.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.native.osx.NativeMacLibrary
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextPane
|
import com.formdev.flatlaf.extras.components.FlatTextPane
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
@@ -7,7 +8,6 @@ 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.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.JXLabel
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
@@ -57,6 +57,7 @@ object OptionPane {
|
|||||||
pane.selectInitialValue()
|
pane.selectInitialValue()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
dialog.setLocationRelativeTo(parentComponent)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
dialog.dispose()
|
dialog.dispose()
|
||||||
val selectedValue = pane.value
|
val selectedValue = pane.value
|
||||||
@@ -114,6 +115,36 @@ object OptionPane {
|
|||||||
dialog.dispose()
|
dialog.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showInputDialog(
|
||||||
|
parentComponent: Component?,
|
||||||
|
title: String = UIManager.getString("OptionPane.messageDialogTitle"),
|
||||||
|
value: String = StringUtils.EMPTY,
|
||||||
|
placeholder: String = StringUtils.EMPTY,
|
||||||
|
): String? {
|
||||||
|
val pane = JOptionPane(StringUtils.EMPTY, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION)
|
||||||
|
val dialog = initDialog(pane.createDialog(parentComponent, title))
|
||||||
|
pane.wantsInput = true
|
||||||
|
pane.initialSelectionValue = value
|
||||||
|
|
||||||
|
val textField = SwingUtils.getDescendantsOfType(JTextField::class.java, pane, true).firstOrNull()
|
||||||
|
if (textField?.name == "OptionPane.textField") {
|
||||||
|
textField.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(0, 0, 2, 0)
|
||||||
|
)
|
||||||
|
textField.background = UIManager.getColor("window")
|
||||||
|
textField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.isVisible = true
|
||||||
|
dialog.dispose()
|
||||||
|
|
||||||
|
val inputValue = pane.inputValue
|
||||||
|
if (inputValue == JOptionPane.UNINITIALIZED_VALUE) return null
|
||||||
|
|
||||||
|
return inputValue as? String
|
||||||
|
}
|
||||||
|
|
||||||
fun openFileInFolder(
|
fun openFileInFolder(
|
||||||
parentComponent: Component,
|
parentComponent: Component,
|
||||||
file: File,
|
file: File,
|
||||||
@@ -141,14 +172,31 @@ object OptionPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initDialog(dialog: JDialog): JDialog {
|
private fun initDialog(dialog: JDialog): JDialog {
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
dialog.rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false)
|
||||||
|
dialog.rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.TITLE_BAR_HEIGHT,
|
||||||
|
UIManager.getInt("TabbedPane.tabHeight")
|
||||||
|
)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
dialog.rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||||
|
dialog.rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||||
|
dialog.rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||||
|
dialog.rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||||
|
)
|
||||||
|
|
||||||
if (JBR.isWindowDecorationsSupported()) {
|
|
||||||
|
|
||||||
val windowDecorations = JBR.getWindowDecorations()
|
val height = UIManager.getInt("TabbedPane.tabHeight") - 10
|
||||||
val titleBar = windowDecorations.createCustomTitleBar()
|
if (JBR.isWindowDecorationsSupported()) {
|
||||||
titleBar.putProperty("controls.visible", false)
|
val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar()
|
||||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight") - if (SystemInfo.isMacOS) 10f else 6f
|
customTitleBar.putProperty("controls.visible", false)
|
||||||
windowDecorations.setCustomTitleBar(dialog, titleBar)
|
customTitleBar.height = height.toFloat()
|
||||||
|
JBR.getWindowDecorations().setCustomTitleBar(dialog, customTitleBar)
|
||||||
|
} else {
|
||||||
|
NativeMacLibrary.setControlsVisible(dialog, false)
|
||||||
|
}
|
||||||
|
|
||||||
val label = JLabel(dialog.title)
|
val label = JLabel(dialog.title)
|
||||||
label.putClientProperty(FlatClientProperties.STYLE, "font: bold")
|
label.putClientProperty(FlatClientProperties.STYLE, "font: bold")
|
||||||
@@ -156,11 +204,9 @@ object OptionPane {
|
|||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
box.add(label)
|
box.add(label)
|
||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
box.preferredSize = Dimension(-1, titleBar.height.toInt())
|
box.preferredSize = Dimension(-1, height)
|
||||||
|
|
||||||
dialog.contentPane.add(box, BorderLayout.NORTH)
|
dialog.contentPane.add(box, BorderLayout.NORTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
abstract class PropertyTerminalTab : TerminalTab {
|
abstract class PropertyTerminalTab : TerminalTab {
|
||||||
protected val listeners = mutableListOf<PropertyChangeListener>()
|
protected val listeners = mutableListOf<PropertyChangeListener>()
|
||||||
@@ -26,6 +30,10 @@ abstract class PropertyTerminalTab : TerminalTab {
|
|||||||
|
|
||||||
override fun onLostFocus() {
|
override fun onLostFocus() {
|
||||||
hasFocus = false
|
hasFocus = false
|
||||||
|
|
||||||
|
// 切换标签时,尝试隐藏悬浮工具栏
|
||||||
|
val evt = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||||
|
evt.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerHide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ class PtyConnectorFactory : Disposable {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
|
private val log = LoggerFactory.getLogger(PtyConnectorFactory::class.java)
|
||||||
fun getInstance(scope: Scope): PtyConnectorFactory {
|
fun getInstance(): PtyConnectorFactory {
|
||||||
return scope.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(PtyConnectorFactory::class) { PtyConnectorFactory() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +28,27 @@ class PtyConnectorFactory : Disposable {
|
|||||||
rows: Int = 24, cols: Int = 80,
|
rows: Int = 24, cols: Int = 80,
|
||||||
env: Map<String, String> = emptyMap(),
|
env: Map<String, String> = emptyMap(),
|
||||||
charset: Charset = StandardCharsets.UTF_8
|
charset: Charset = StandardCharsets.UTF_8
|
||||||
|
): PtyConnector {
|
||||||
|
val command = database.terminal.localShell
|
||||||
|
val commands = mutableListOf(command)
|
||||||
|
if (SystemUtils.IS_OS_UNIX) {
|
||||||
|
commands.add("-l")
|
||||||
|
}
|
||||||
|
return createPtyConnector(
|
||||||
|
commands = commands.toTypedArray(),
|
||||||
|
rows = rows,
|
||||||
|
cols = cols,
|
||||||
|
env = env,
|
||||||
|
charset = charset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPtyConnector(
|
||||||
|
commands: Array<String>,
|
||||||
|
rows: Int = 24, cols: Int = 80,
|
||||||
|
env: Map<String, String> = emptyMap(),
|
||||||
|
directory: String = SystemUtils.USER_HOME,
|
||||||
|
charset: Charset = StandardCharsets.UTF_8,
|
||||||
): PtyConnector {
|
): PtyConnector {
|
||||||
val envs = mutableMapOf<String, String>()
|
val envs = mutableMapOf<String, String>()
|
||||||
envs.putAll(System.getenv())
|
envs.putAll(System.getenv())
|
||||||
@@ -44,22 +66,16 @@ class PtyConnectorFactory : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val command = database.terminal.localShell
|
|
||||||
val commands = mutableListOf(command)
|
|
||||||
if (SystemUtils.IS_OS_UNIX) {
|
|
||||||
commands.add("-l")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("command: {} , envs: {}", commands.joinToString(" "), envs)
|
log.debug("command: {} , envs: {}", commands.joinToString(" "), envs)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ptyProcess = PtyProcessBuilder(commands.toTypedArray())
|
val ptyProcess = PtyProcessBuilder(commands)
|
||||||
.setEnvironment(envs)
|
.setEnvironment(envs)
|
||||||
.setInitialRows(rows)
|
.setInitialRows(rows)
|
||||||
.setInitialColumns(cols)
|
.setInitialColumns(cols)
|
||||||
.setConsole(false)
|
.setConsole(false)
|
||||||
.setDirectory(SystemUtils.USER_HOME)
|
.setDirectory(StringUtils.defaultIfBlank(directory, SystemUtils.USER_HOME))
|
||||||
.setCygwin(false)
|
.setCygwin(false)
|
||||||
.setUseWinConPty(SystemUtils.IS_OS_WINDOWS)
|
.setUseWinConPty(SystemUtils.IS_OS_WINDOWS)
|
||||||
.setRedirectErrorStream(false)
|
.setRedirectErrorStream(false)
|
||||||
@@ -71,20 +87,14 @@ class PtyConnectorFactory : Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun decorate(ptyConnector: PtyConnector): PtyConnector {
|
fun decorate(ptyConnector: PtyConnector): PtyConnector {
|
||||||
// 集成转发,如果PtyConnector支持转发那么应该在当前注释行前面代理
|
// 宏
|
||||||
val multiplePtyConnector = MultiplePtyConnector(ptyConnector)
|
val macroPtyConnector = MacroPtyConnector(ptyConnector)
|
||||||
// 宏应该在转发前面执行,不然会导致重复录制
|
|
||||||
val macroPtyConnector = MacroPtyConnector(multiplePtyConnector)
|
|
||||||
// 集成自动删除
|
// 集成自动删除
|
||||||
val autoRemovePtyConnector = AutoRemovePtyConnector(macroPtyConnector)
|
val autoRemovePtyConnector = AutoRemovePtyConnector(macroPtyConnector)
|
||||||
ptyConnectors.add(autoRemovePtyConnector)
|
ptyConnectors.add(autoRemovePtyConnector)
|
||||||
return autoRemovePtyConnector
|
return autoRemovePtyConnector
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPtyConnectors(): List<PtyConnector> {
|
|
||||||
return ptyConnectors
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class AutoRemovePtyConnector(connector: PtyConnector) : PtyConnectorDelegate(connector) {
|
private inner class AutoRemovePtyConnector(connector: PtyConnector) : PtyConnectorDelegate(connector) {
|
||||||
override fun close() {
|
override fun close() {
|
||||||
ptyConnectors.remove(this)
|
ptyConnectors.remove(this)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
@@ -12,7 +13,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
abstract class PtyHostTerminalTab(
|
abstract class PtyHostTerminalTab(
|
||||||
windowScope: WindowScope,
|
windowScope: WindowScope,
|
||||||
host: Host,
|
host: Host,
|
||||||
terminal: Terminal = TerminalFactory.getInstance(windowScope).createTerminal()
|
terminal: Terminal = TerminalFactory.getInstance().createTerminal()
|
||||||
) : HostTerminalTab(windowScope, host, terminal) {
|
) : HostTerminalTab(windowScope, host, terminal) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -22,14 +23,8 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
private var readerJob: Job? = null
|
private var readerJob: Job? = null
|
||||||
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
private val ptyConnectorDelegate = PtyConnectorDelegate()
|
||||||
|
protected val terminalPanel = TerminalPanelFactory.getInstance().createTerminalPanel(terminal, ptyConnectorDelegate)
|
||||||
protected val terminalPanel =
|
protected val ptyConnectorFactory get() = PtyConnectorFactory.getInstance()
|
||||||
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) {
|
||||||
@@ -49,7 +44,7 @@ abstract class PtyHostTerminalTab(
|
|||||||
startPtyConnectorReader()
|
startPtyConnectorReader()
|
||||||
|
|
||||||
// 启动命令
|
// 启动命令
|
||||||
if (host.options.startupCommand.isNotBlank()) {
|
if (host.options.startupCommand.isNotBlank() && host.protocol != Protocol.SFTPPty) {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
delay(250.milliseconds)
|
delay(250.milliseconds)
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
@@ -120,9 +115,9 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
stop()
|
stop()
|
||||||
|
Disposer.dispose(terminalPanel)
|
||||||
super.dispose()
|
super.dispose()
|
||||||
|
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("Host: {} disposed", host.name)
|
log.info("Host: {} disposed", host.name)
|
||||||
}
|
}
|
||||||
@@ -133,4 +128,14 @@ abstract class PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract suspend fun openPtyConnector(): PtyConnector
|
abstract suspend fun openPtyConnector(): PtyConnector
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == DataProviders.TerminalPanel) {
|
||||||
|
return terminalPanel as T?
|
||||||
|
} else if (dataKey == DataProviders.TerminalWriter) {
|
||||||
|
return terminalPanel.getData(DataKey.TerminalWriter) as T?
|
||||||
|
}
|
||||||
|
return super.getData(dataKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
16
src/main/kotlin/app/termora/RememberFocusTerminalTab.kt
Normal file
16
src/main/kotlin/app/termora/RememberFocusTerminalTab.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
|
||||||
|
abstract class RememberFocusTerminalTab : TerminalTab {
|
||||||
|
private var lastFocusedComponent: Component? = null
|
||||||
|
|
||||||
|
override fun onLostFocus() {
|
||||||
|
lastFocusedComponent = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGrabFocus() {
|
||||||
|
lastFocusedComponent?.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
161
src/main/kotlin/app/termora/RequestAuthenticationDialog.kt
Normal file
161
src/main/kotlin/app/termora/RequestAuthenticationDialog.kt
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPair
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
|
import javax.swing.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(owner) {
|
||||||
|
|
||||||
|
private val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
private val rememberCheckBox = JCheckBox("Remember")
|
||||||
|
private val passwordPanel = JPanel(BorderLayout())
|
||||||
|
private val passwordPasswordField = OutlinePasswordField()
|
||||||
|
private val usernameTextField = OutlineTextField()
|
||||||
|
private val publicKeyComboBox = OutlineComboBox<OhKeyPair>()
|
||||||
|
private val keyManager get() = KeyManager.getInstance()
|
||||||
|
private var authentication = Authentication.No
|
||||||
|
|
||||||
|
init {
|
||||||
|
isModal = true
|
||||||
|
title = "SSH User Authentication"
|
||||||
|
controlsVisible = false
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
pack()
|
||||||
|
|
||||||
|
size = Dimension(max(380, size.width), size.height)
|
||||||
|
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
if (value is OhKeyPair) value.name else value,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (keyPair in keyManager.getOhKeyPairs()) {
|
||||||
|
publicKeyComboBox.addItem(keyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
switchPasswordComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameTextField.text = host.username
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.PublicKey)
|
||||||
|
val formMargin = "7dlu"
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
switchPasswordComponent()
|
||||||
|
|
||||||
|
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
||||||
|
.layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
|
||||||
|
.add(authenticationTypeComboBox).xy(3, 1)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, 3)
|
||||||
|
.add(usernameTextField).xy(3, 3)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 5)
|
||||||
|
.add(passwordPanel).xy(3, 5)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchPasswordComponent() {
|
||||||
|
passwordPanel.removeAll()
|
||||||
|
if (authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||||
|
passwordPanel.add(passwordPasswordField, BorderLayout.CENTER)
|
||||||
|
} else if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
|
passwordPanel.add(publicKeyComboBox, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
passwordPanel.revalidate()
|
||||||
|
passwordPanel.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSouthPanel(): JComponent? {
|
||||||
|
val box = super.createSouthPanel() ?: return null
|
||||||
|
rememberCheckBox.isFocusable = false
|
||||||
|
box.add(rememberCheckBox, 0)
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
authentication = Authentication.No
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
val type = authenticationTypeComboBox.selectedItem as AuthenticationType
|
||||||
|
|
||||||
|
if (type == AuthenticationType.Password) {
|
||||||
|
if (passwordPasswordField.password.isEmpty()) {
|
||||||
|
passwordPasswordField.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (type == AuthenticationType.PublicKey) {
|
||||||
|
if (publicKeyComboBox.selectedItem == null) {
|
||||||
|
publicKeyComboBox.requestFocusInWindow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = type,
|
||||||
|
password = if (type == AuthenticationType.Password) String(passwordPasswordField.password)
|
||||||
|
else (publicKeyComboBox.selectedItem as OhKeyPair).id
|
||||||
|
)
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthentication(): Authentication {
|
||||||
|
isModal = true
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
if (usernameTextField.text.isBlank()) {
|
||||||
|
usernameTextField.requestFocusInWindow()
|
||||||
|
} else {
|
||||||
|
passwordPasswordField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isVisible = true
|
||||||
|
return authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRemembered(): Boolean {
|
||||||
|
return rememberCheckBox.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUsername(): String {
|
||||||
|
return usernameTextField.text
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
212
src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt
Normal file
212
src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
|
import app.termora.terminal.*
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import org.apache.commons.io.Charsets
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.apache.sshd.client.ClientBuilder
|
||||||
|
import org.apache.sshd.client.SshClient
|
||||||
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter
|
||||||
|
import org.apache.sshd.common.util.net.SshdSocketAddress
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
||||||
|
private val keyManager by lazy { KeyManager.getInstance() }
|
||||||
|
private val tempFiles = mutableListOf<Path>()
|
||||||
|
private var sshClient: SshClient? = null
|
||||||
|
private var sshSession: ClientSession? = null
|
||||||
|
private var lastPasswordReporterDataListener: PasswordReporterDataListener? = null
|
||||||
|
private val sftpCommand get() = Database.getDatabase().sftp.sftpCommand
|
||||||
|
private val defaultDirectory get() = Database.getDatabase().sftp.defaultDirectory
|
||||||
|
|
||||||
|
init {
|
||||||
|
terminalPanel.dropFiles = true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val canSupports by lazy {
|
||||||
|
val process = if (SystemInfo.isWindows) {
|
||||||
|
ProcessBuilder("cmd.exe", "/c", "where", "sftp").start()
|
||||||
|
} else {
|
||||||
|
ProcessBuilder("which", "sftp").start()
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
return@lazy process.exitValue() == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
|
|
||||||
|
val useJumpHosts = host.options.jumpHosts.isNotEmpty() || host.proxy.type != ProxyType.No
|
||||||
|
val commands = mutableListOf(StringUtils.defaultIfBlank(sftpCommand, "sftp"))
|
||||||
|
var host = this.host
|
||||||
|
|
||||||
|
// 如果配置了跳板机或者代理,那么通过 SSH 的端口转发到本地
|
||||||
|
if (useJumpHosts) {
|
||||||
|
host = host.copy(
|
||||||
|
updateDate = System.currentTimeMillis(),
|
||||||
|
tunnelings = listOf(
|
||||||
|
Tunneling(
|
||||||
|
type = TunnelingType.Local,
|
||||||
|
sourceHost = SshdSocketAddress.LOCALHOST_NAME,
|
||||||
|
destinationHost = SshdSocketAddress.LOCALHOST_NAME,
|
||||||
|
destinationPort = host.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val sshClient = SshClients.openClient(host).apply { sshClient = this }
|
||||||
|
val sshSession = SshClients.openSession(host, sshClient).apply { sshSession = this }
|
||||||
|
|
||||||
|
// 打开通道
|
||||||
|
for (tunneling in host.tunnelings) {
|
||||||
|
val address = SshClients.openTunneling(sshSession, host, tunneling)
|
||||||
|
host = host.copy(
|
||||||
|
host = address.hostName,
|
||||||
|
port = address.port,
|
||||||
|
updateDate = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (useJumpHosts) {
|
||||||
|
// 打开通道后忽略 key 检查
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("StrictHostKeyChecking=no")
|
||||||
|
|
||||||
|
// 不保存 known_hosts
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("UserKnownHostsFile=${if (SystemInfo.isWindows) "NUL" else "/dev/null"}")
|
||||||
|
} else {
|
||||||
|
// known_hosts
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("UserKnownHostsFile=${File(Application.getBaseDataDir(), "known_hosts").absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compression
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("Compression=yes")
|
||||||
|
|
||||||
|
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题
|
||||||
|
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).joinToString(",") { it.name }
|
||||||
|
commands.add("-o")
|
||||||
|
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms}")
|
||||||
|
|
||||||
|
// 不使用配置文件
|
||||||
|
commands.add("-F")
|
||||||
|
commands.add("/dev/null")
|
||||||
|
|
||||||
|
// port
|
||||||
|
commands.add("-P")
|
||||||
|
commands.add(host.port.toString())
|
||||||
|
|
||||||
|
// 设置认证信息
|
||||||
|
setAuthentication(commands, host)
|
||||||
|
|
||||||
|
|
||||||
|
val envs = host.options.envs()
|
||||||
|
if (envs.containsKey("CurrentDir")) {
|
||||||
|
val currentDir = envs.getValue("CurrentDir")
|
||||||
|
commands.add("${host.username}@${host.host}:${currentDir}")
|
||||||
|
} else if (host.options.sftpDefaultDirectory.isNotBlank()) {
|
||||||
|
commands.add("${host.username}@${host.host}:${host.options.sftpDefaultDirectory.trim()}")
|
||||||
|
} else {
|
||||||
|
commands.add("${host.username}@${host.host}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val directory = FileUtils.getFile(StringUtils.defaultIfBlank(defaultDirectory, SystemUtils.USER_HOME))
|
||||||
|
|
||||||
|
val winSize = terminalPanel.winSize()
|
||||||
|
val ptyConnector = ptyConnectorFactory.createPtyConnector(
|
||||||
|
commands = commands.toTypedArray(),
|
||||||
|
rows = winSize.rows, cols = winSize.cols,
|
||||||
|
env = host.options.envs(),
|
||||||
|
charset = Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
|
||||||
|
directory = if (directory.exists()) directory.absolutePath else SystemUtils.USER_HOME
|
||||||
|
)
|
||||||
|
|
||||||
|
return ptyConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAuthentication(commands: MutableList<String>, host: Host) {
|
||||||
|
// 如果通过公钥连接
|
||||||
|
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
|
val ohKeyPair = keyManager.getOhKeyPair(host.authentication.password)
|
||||||
|
if (ohKeyPair != null) {
|
||||||
|
val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(ohKeyPair)
|
||||||
|
val privateKeyPath = Application.createSubTemporaryDir()
|
||||||
|
val privateKeyFile = Files.createTempFile(privateKeyPath, Application.getName(), StringUtils.EMPTY)
|
||||||
|
Files.newOutputStream(privateKeyFile)
|
||||||
|
.use { OpenSSHKeyPairResourceWriter.INSTANCE.writePrivateKey(keyPair, null, null, it) }
|
||||||
|
commands.add("-i")
|
||||||
|
commands.add(privateKeyFile.toFile().absolutePath)
|
||||||
|
tempFiles.add(privateKeyPath)
|
||||||
|
}
|
||||||
|
} else if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
terminal.getTerminalModel().addDataListener(PasswordReporterDataListener(host).apply {
|
||||||
|
lastPasswordReporterDataListener = this
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
// 删除密码监听
|
||||||
|
lastPasswordReporterDataListener?.let { listener ->
|
||||||
|
SwingUtilities.invokeLater { terminal.getTerminalModel().removeDataListener(listener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
IOUtils.closeQuietly(sshSession)
|
||||||
|
IOUtils.closeQuietly(sshClient)
|
||||||
|
|
||||||
|
tempFiles.removeIf {
|
||||||
|
FileUtils.deleteQuietly(it.toFile())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
super.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(): Icon {
|
||||||
|
return Icons.fileFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class PasswordReporterDataListener(private val host: Host) : DataListener {
|
||||||
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
|
if (key == VisualTerminal.Written && data is String) {
|
||||||
|
|
||||||
|
// 要求输入密码
|
||||||
|
val line = terminal.getDocument().getScreenLine(terminal.getCursorModel().getPosition().y)
|
||||||
|
if (line.getText().trim().trimIndent().startsWith("${host.username}@${host.host}'s password:")) {
|
||||||
|
|
||||||
|
// 删除密码监听
|
||||||
|
terminal.getTerminalModel().removeDataListener(this)
|
||||||
|
|
||||||
|
val ptyConnector = getPtyConnector()
|
||||||
|
|
||||||
|
// password
|
||||||
|
ptyConnector.write(host.authentication.password.toByteArray(ptyConnector.getCharset()))
|
||||||
|
|
||||||
|
// enter
|
||||||
|
ptyConnector.write(
|
||||||
|
terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
|
||||||
|
.toByteArray(ptyConnector.getCharset())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import app.termora.transport.TransportDataProviders
|
|
||||||
import app.termora.transport.TransportPanel
|
|
||||||
import java.beans.PropertyChangeListener
|
|
||||||
import javax.swing.Icon
|
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.JOptionPane
|
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
class SFTPTerminalTab : Disposable, TerminalTab {
|
|
||||||
|
|
||||||
private val transportPanel by lazy {
|
|
||||||
TransportPanel().apply {
|
|
||||||
Disposer.register(this@SFTPTerminalTab, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(): String {
|
|
||||||
return "SFTP"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIcon(): Icon {
|
|
||||||
return Icons.folder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
|
||||||
return transportPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canClone(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canClose(): Boolean {
|
|
||||||
assertEventDispatchThread()
|
|
||||||
val transportManager = transportPanel.getData(TransportDataProviders.TransportManager) ?: return true
|
|
||||||
if (transportManager.getTransports().isEmpty()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return OptionPane.showConfirmDialog(
|
|
||||||
SwingUtilities.getWindowAncestor(getJComponent()),
|
|
||||||
I18n.getString("termora.transport.sftp.close-tab"),
|
|
||||||
messageType = JOptionPane.QUESTION_MESSAGE,
|
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
|
||||||
) == JOptionPane.OK_OPTION
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.actions.TabReconnectAction
|
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.keyboardinteractive.TerminalUserInteraction
|
||||||
@@ -24,23 +26,30 @@ import org.apache.sshd.common.channel.ChannelListener
|
|||||||
import org.apache.sshd.common.session.Session
|
import org.apache.sshd.common.session.Session
|
||||||
import org.apache.sshd.common.session.SessionListener
|
import org.apache.sshd.common.session.SessionListener
|
||||||
import org.apache.sshd.common.session.SessionListener.Event
|
import org.apache.sshd.common.session.SessionListener.Event
|
||||||
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 java.util.*
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
|
||||||
class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(PtyHostTerminalTab::class.java)
|
val SSHSession = DataKey(ClientSession::class)
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
private val tab = this
|
||||||
|
|
||||||
private var sshClient: SshClient? = null
|
private var sshClient: SshClient? = null
|
||||||
private var sshSession: ClientSession? = null
|
private var sshSession: ClientSession? = null
|
||||||
private var sshChannelShell: ChannelShell? = null
|
private var sshChannelShell: ChannelShell? = null
|
||||||
|
private val terminalTabbedManager
|
||||||
|
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||||
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
@@ -80,9 +89,34 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("SSH client is opening...\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var host =
|
||||||
|
this.host.copy(authentication = this.host.authentication.copy(), updateDate = System.currentTimeMillis())
|
||||||
|
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
val client = SshClients.openClient(host).also { sshClient = it }
|
val client = SshClients.openClient(host).also { sshClient = it }
|
||||||
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
// keyboard interactive
|
// keyboard interactive
|
||||||
client.userInteraction = TerminalUserInteraction(SwingUtilities.getWindowAncestor(terminalPanel))
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.No) {
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
val dialog = RequestAuthenticationDialog(owner, host)
|
||||||
|
val authentication = dialog.getAuthentication()
|
||||||
|
host = host.copy(
|
||||||
|
authentication = authentication,
|
||||||
|
username = dialog.getUsername(),
|
||||||
|
updateDate = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
// save
|
||||||
|
if (dialog.isRemembered()) {
|
||||||
|
HostManager.getInstance().addHost(
|
||||||
|
tab.host.copy(
|
||||||
|
authentication = authentication,
|
||||||
|
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val sessionListener = MySessionListener()
|
val sessionListener = MySessionListener()
|
||||||
val channelListener = MyChannelListener()
|
val channelListener = MyChannelListener()
|
||||||
@@ -119,13 +153,25 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
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\r\n${ControlCharacters.ESC}[31m")
|
terminal.write("\r\n\r\n${ControlCharacters.ESC}[31m")
|
||||||
terminal.write("Channel has been disconnected.")
|
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
|
||||||
if (reconnectShortcut is KeyShortcut) {
|
if (reconnectShortcut is KeyShortcut) {
|
||||||
terminal.write(" Type $reconnectShortcut to reconnect.")
|
terminal.write(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.terminal.channel-reconnect",
|
||||||
|
reconnectShortcut.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
terminal.write("\r\n")
|
terminal.write("\r\n")
|
||||||
terminal.write("${ControlCharacters.ESC}[0m")
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
terminalModel.setData(DataKey.ShowCursor, false)
|
terminalModel.setData(DataKey.ShowCursor, false)
|
||||||
|
if (Database.getDatabase().terminal.autoCloseTabWhenDisconnected) {
|
||||||
|
terminalTabbedManager?.let { manager ->
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
manager.closeTerminalTab(tab, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -159,35 +205,30 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (tunneling in host.tunnelings) {
|
for (tunneling in host.tunnelings) {
|
||||||
if (tunneling.type == TunnelingType.Local) {
|
try {
|
||||||
session.startLocalPortForwarding(
|
SshClients.openTunneling(session, host, tunneling)
|
||||||
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
withContext(Dispatchers.Swing) {
|
||||||
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort)
|
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
||||||
)
|
}
|
||||||
} else if (tunneling.type == TunnelingType.Remote) {
|
} catch (e: Exception) {
|
||||||
session.startRemotePortForwarding(
|
if (log.isErrorEnabled) {
|
||||||
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
log.error("Start [${tunneling.name}] port forwarding failed: {}", e.message, e)
|
||||||
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort),
|
}
|
||||||
)
|
withContext(Dispatchers.Swing) {
|
||||||
} else if (tunneling.type == TunnelingType.Dynamic) {
|
terminal.write("Start [${tunneling.name}] port forwarding failed: ${e.message}\r\n")
|
||||||
session.startDynamicPortForwarding(
|
}
|
||||||
SshdSocketAddress(
|
|
||||||
tunneling.sourceHost,
|
|
||||||
tunneling.sourcePort
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
|
||||||
log.info("SSH [{}] started {} port forwarding.", host.name, tunneling.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
terminal.write("Start [${tunneling.name}] port forwarding successfully.\r\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
|
if (dataKey == SSHSession) {
|
||||||
|
return sshSession as T?
|
||||||
|
}
|
||||||
|
return super.getData(dataKey)
|
||||||
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
package app.termora
|
|
||||||
|
|
||||||
import javax.swing.event.TreeModelEvent
|
|
||||||
import javax.swing.event.TreeModelListener
|
|
||||||
import javax.swing.tree.TreeModel
|
|
||||||
import javax.swing.tree.TreePath
|
|
||||||
|
|
||||||
class SearchableHostTreeModel(
|
|
||||||
private val model: HostTreeModel,
|
|
||||||
private val filter: (host: Host) -> Boolean = { true }
|
|
||||||
) : TreeModel {
|
|
||||||
private var text = String()
|
|
||||||
|
|
||||||
override fun getRoot(): Any {
|
|
||||||
return model.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChild(parent: Any?, index: Int): Any {
|
|
||||||
return getChildren(parent)[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChildCount(parent: Any?): Int {
|
|
||||||
return getChildren(parent).size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isLeaf(node: Any?): Boolean {
|
|
||||||
return model.isLeaf(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun valueForPathChanged(path: TreePath?, newValue: Any?) {
|
|
||||||
return model.valueForPathChanged(path, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIndexOfChild(parent: Any?, child: Any?): Int {
|
|
||||||
return getChildren(parent).indexOf(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addTreeModelListener(l: TreeModelListener) {
|
|
||||||
model.addTreeModelListener(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeTreeModelListener(l: TreeModelListener) {
|
|
||||||
model.removeTreeModelListener(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getChildren(parent: Any?): List<Host> {
|
|
||||||
val children = model.getChildren(parent)
|
|
||||||
if (children.isEmpty()) return emptyList()
|
|
||||||
return children.filter { e ->
|
|
||||||
filter.invoke(e)
|
|
||||||
&& e.name.contains(text, true)
|
|
||||||
|| e.host.contains(text, true)
|
|
||||||
|| TreeUtils.children(model, e, true).filterIsInstance<Host>().any { it.name.contains(text, true) || it.host.contains(text, true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(text: String) {
|
|
||||||
this.text = text
|
|
||||||
model.listeners.forEach {
|
|
||||||
it.treeStructureChanged(
|
|
||||||
TreeModelEvent(
|
|
||||||
this, TreePath(root),
|
|
||||||
null, null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,8 @@ import org.apache.commons.io.Charsets
|
|||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
class SerialTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val serialPort = Serials.openPort(host)
|
val serialPort = Serials.openPort(host)
|
||||||
return SerialPortPtyConnector(
|
return SerialPortPtyConnector(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.AES.encodeBase64String
|
|||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.highlight.KeywordHighlight
|
import app.termora.highlight.KeywordHighlight
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
import app.termora.keymap.Keymap
|
import app.termora.keymap.Keymap
|
||||||
@@ -14,14 +15,19 @@ import app.termora.keymgr.OhKeyPair
|
|||||||
import app.termora.macro.Macro
|
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.sftp.SFTPTab
|
||||||
|
import app.termora.snippet.Snippet
|
||||||
|
import app.termora.snippet.SnippetManager
|
||||||
import app.termora.sync.SyncConfig
|
import app.termora.sync.SyncConfig
|
||||||
import app.termora.sync.SyncRange
|
import app.termora.sync.SyncRange
|
||||||
import app.termora.sync.SyncType
|
import app.termora.sync.SyncType
|
||||||
import app.termora.sync.SyncerProvider
|
import app.termora.sync.SyncerProvider
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.terminal.panel.FloatingToolbarPanel
|
||||||
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.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.formdev.flatlaf.extras.components.*
|
import com.formdev.flatlaf.extras.components.*
|
||||||
import com.formdev.flatlaf.util.FontUtils
|
import com.formdev.flatlaf.util.FontUtils
|
||||||
@@ -32,8 +38,8 @@ import com.jthemedetecor.OsThemeDetector
|
|||||||
import com.sun.jna.LastErrorException
|
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.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
import org.apache.commons.codec.binary.Base64
|
||||||
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
|
||||||
@@ -43,8 +49,12 @@ 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.Dimension
|
||||||
|
import java.awt.Toolkit
|
||||||
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.awt.event.ItemListener
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@@ -60,6 +70,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane)
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
private val hostManager get() = HostManager.getInstance()
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val snippetManager get() = SnippetManager.getInstance()
|
||||||
private val keymapManager get() = KeymapManager.getInstance()
|
private val keymapManager get() = KeymapManager.getInstance()
|
||||||
private val macroManager get() = MacroManager.getInstance()
|
private val macroManager get() = MacroManager.getInstance()
|
||||||
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
private val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
@@ -107,6 +118,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
addOption(AppearanceOption())
|
addOption(AppearanceOption())
|
||||||
addOption(TerminalOption())
|
addOption(TerminalOption())
|
||||||
addOption(KeyShortcutsOption())
|
addOption(KeyShortcutsOption())
|
||||||
|
addOption(SFTPOption())
|
||||||
addOption(CloudSyncOption())
|
addOption(CloudSyncOption())
|
||||||
addOption(DoormanOption())
|
addOption(DoormanOption())
|
||||||
addOption(AboutOption())
|
addOption(AboutOption())
|
||||||
@@ -189,12 +201,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
appearance.language = languageComboBox.selectedItem as String
|
appearance.language = languageComboBox.selectedItem as String
|
||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
OptionPane.showMessageDialog(
|
TermoraRestarter.getInstance().scheduleRestart(owner)
|
||||||
owner,
|
|
||||||
I18n.getString("termora.settings.restart.message"),
|
|
||||||
I18n.getString("termora.settings.restart.title"),
|
|
||||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,12 +305,16 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
private inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||||
private val cursorStyleComboBox = FlatComboBox<CursorStyle>()
|
private val cursorStyleComboBox = FlatComboBox<CursorStyle>()
|
||||||
private val debugComboBox = YesOrNoComboBox()
|
private val debugComboBox = YesOrNoComboBox()
|
||||||
|
private val beepComboBox = YesOrNoComboBox()
|
||||||
|
private val cursorBlinkComboBox = YesOrNoComboBox()
|
||||||
private val fontComboBox = FlatComboBox<String>()
|
private val fontComboBox = FlatComboBox<String>()
|
||||||
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.getDatabase().terminal
|
private val terminalSetting get() = Database.getDatabase().terminal
|
||||||
private val selectCopyComboBox = YesOrNoComboBox()
|
private val selectCopyComboBox = YesOrNoComboBox()
|
||||||
|
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||||
|
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -319,6 +330,26 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoCloseTabComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.autoCloseTabWhenDisconnected = autoCloseTabComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoCloseTabComboBox.toolTipText = I18n.getString("termora.settings.terminal.auto-close-tab-description")
|
||||||
|
|
||||||
|
floatingToolbarComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.floatingToolbar = floatingToolbarComboBox.selectedItem as Boolean
|
||||||
|
TerminalPanelFactory.getInstance().getTerminalPanels().forEach { tp ->
|
||||||
|
if (terminalSetting.floatingToolbar && FloatingToolbarPanel.isPined) {
|
||||||
|
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerShow()
|
||||||
|
} else {
|
||||||
|
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectCopyComboBox.addItemListener { e ->
|
selectCopyComboBox.addItemListener { e ->
|
||||||
if (e.stateChange == ItemEvent.SELECTED) {
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.selectCopy = selectCopyComboBox.selectedItem as Boolean
|
terminalSetting.selectCopy = selectCopyComboBox.selectedItem as Boolean
|
||||||
@@ -338,7 +369,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.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach { e ->
|
TerminalFactory.getInstance().getTerminals().forEach { e ->
|
||||||
e.getTerminalModel().setData(DataKey.CursorStyle, style)
|
e.getTerminalModel().setData(DataKey.CursorStyle, style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,13 +379,26 @@ 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.getInstance(ApplicationScope.forWindowScope(owner)).getTerminals().forEach {
|
TerminalFactory.getInstance().getTerminals().forEach {
|
||||||
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
|
it.getTerminalModel().setData(TerminalPanel.Debug, terminalSetting.debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
beepComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.beep = beepComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorBlinkComboBox.addItemListener { e ->
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.cursorBlink = cursorBlinkComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
shellComboBox.addItemListener {
|
shellComboBox.addItemListener {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.localShell = shellComboBox.selectedItem as String
|
terminalSetting.localShell = shellComboBox.selectedItem as String
|
||||||
@@ -364,10 +408,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fireFontChanged() {
|
private fun fireFontChanged() {
|
||||||
ApplicationScope.windowScopes().forEach {
|
TerminalPanelFactory.getInstance()
|
||||||
TerminalPanelFactory.getInstance(it)
|
|
||||||
.fireResize()
|
.fireResize()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -390,6 +432,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
init {
|
||||||
|
preferredSize = Dimension(preferredSize.width, fontComboBox.preferredSize.height - 2)
|
||||||
|
maximumSize = Dimension(preferredSize.width, preferredSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getListCellRendererComponent(
|
override fun getListCellRendererComponent(
|
||||||
list: JList<*>?,
|
list: JList<*>?,
|
||||||
value: Any?,
|
value: Any?,
|
||||||
@@ -423,28 +470,11 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
shellComboBox.selectedItem = terminalSetting.localShell
|
shellComboBox.selectedItem = terminalSetting.localShell
|
||||||
|
|
||||||
val fonts = linkedSetOf(
|
val fonts = linkedSetOf<String>("JetBrains Mono", "Source Code Pro", "Monospaced")
|
||||||
"JetBrains Mono",
|
FontUtils.getAllFonts().forEach {
|
||||||
"Source Code Pro",
|
if (!fonts.contains(it.family)) {
|
||||||
"Monospaced",
|
fonts.addLast(it.family)
|
||||||
"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) {
|
for (font in fonts) {
|
||||||
@@ -453,8 +483,12 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
fontComboBox.selectedItem = terminalSetting.font
|
fontComboBox.selectedItem = terminalSetting.font
|
||||||
debugComboBox.selectedItem = terminalSetting.debug
|
debugComboBox.selectedItem = terminalSetting.debug
|
||||||
|
beepComboBox.selectedItem = terminalSetting.beep
|
||||||
|
cursorBlinkComboBox.selectedItem = terminalSetting.cursorBlink
|
||||||
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
cursorStyleComboBox.selectedItem = terminalSetting.cursor
|
||||||
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
|
||||||
|
autoCloseTabComboBox.selectedItem = terminalSetting.autoCloseTabWhenDisconnected
|
||||||
|
floatingToolbarComboBox.selectedItem = terminalSetting.floatingToolbar
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -472,9 +506,14 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
|
||||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val beepBtn = JButton(Icons.run)
|
||||||
|
beepBtn.isFocusable = false
|
||||||
|
beepBtn.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON)
|
||||||
|
beepBtn.addActionListener { Toolkit.getDefaultToolkit().beep() }
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
val step = 2
|
val step = 2
|
||||||
val panel = FormBuilder.create().layout(layout)
|
val panel = FormBuilder.create().layout(layout)
|
||||||
@@ -487,10 +526,19 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
||||||
.add(debugComboBox).xy(3, rows).apply { rows += step }
|
.add(debugComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.beep")}:").xy(1, rows)
|
||||||
|
.add(beepComboBox).xy(3, rows)
|
||||||
|
.add(beepBtn).xy(5, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||||
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows)
|
||||||
|
.add(cursorBlinkComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.floating-toolbar")}:").xy(1, rows)
|
||||||
|
.add(floatingToolbarComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.auto-close-tab")}:").xy(1, rows)
|
||||||
|
.add(autoCloseTabComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.local-shell")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.local-shell")}:").xy(1, rows)
|
||||||
.add(shellComboBox).xyw(3, rows, 5)
|
.add(shellComboBox).xyw(3, rows, 5)
|
||||||
.build()
|
.build()
|
||||||
@@ -514,6 +562,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val sync get() = database.sync
|
val sync get() = database.sync
|
||||||
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
||||||
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
||||||
|
val snippetsCheckBox = JCheckBox(I18n.getString("termora.snippet.title"))
|
||||||
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 keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
|
||||||
@@ -550,12 +599,6 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.Gitee) {
|
|
||||||
gistTextField.trailingComponent = null
|
|
||||||
} else {
|
|
||||||
gistTextField.trailingComponent = visitGistBtn
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAll()
|
removeAll()
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
revalidate()
|
revalidate()
|
||||||
@@ -624,6 +667,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
keysCheckBox.addActionListener { refreshButtons() }
|
keysCheckBox.addActionListener { refreshButtons() }
|
||||||
hostsCheckBox.addActionListener { refreshButtons() }
|
hostsCheckBox.addActionListener { refreshButtons() }
|
||||||
|
snippetsCheckBox.addActionListener { refreshButtons() }
|
||||||
keywordHighlightsCheckBox.addActionListener { refreshButtons() }
|
keywordHighlightsCheckBox.addActionListener { refreshButtons() }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -631,6 +675,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun refreshButtons() {
|
private fun refreshButtons() {
|
||||||
sync.rangeKeyPairs = keysCheckBox.isSelected
|
sync.rangeKeyPairs = keysCheckBox.isSelected
|
||||||
sync.rangeHosts = hostsCheckBox.isSelected
|
sync.rangeHosts = hostsCheckBox.isSelected
|
||||||
|
sync.rangeSnippets = snippetsCheckBox.isSelected
|
||||||
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
|
||||||
|
|
||||||
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||||
@@ -642,13 +687,40 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun export() {
|
private fun export() {
|
||||||
|
|
||||||
|
assertEventDispatchThread()
|
||||||
|
|
||||||
|
val passwordField = OutlinePasswordField()
|
||||||
|
val panel = object : JPanel(BorderLayout()) {
|
||||||
|
override fun requestFocusInWindow(): Boolean {
|
||||||
|
return passwordField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = JLabel(I18n.getString("termora.settings.sync.export-encrypt") + StringUtils.SPACE.repeat(25))
|
||||||
|
label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
panel.add(label, BorderLayout.NORTH)
|
||||||
|
panel.add(passwordField, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
var password = StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
panel,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
initialValue = passwordField
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
password = String(passwordField.password).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val fileChooser = FileChooser()
|
val fileChooser = FileChooser()
|
||||||
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
fileChooser.win32Filters.add(Pair("All Files", listOf("*")))
|
fileChooser.win32Filters.add(Pair("All Files", listOf("*")))
|
||||||
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
||||||
fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file ->
|
fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file ->
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
SwingUtilities.invokeLater { exportText(file) }
|
SwingUtilities.invokeLater { exportText(file, password) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,6 +737,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
private fun importFromFile(file: File) {
|
private fun importFromFile(file: File) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return
|
return
|
||||||
@@ -695,7 +768,79 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val json = jsonResult.getOrNull() ?: return
|
var json = jsonResult.getOrNull() ?: return
|
||||||
|
|
||||||
|
// 如果加密了 则解密数据
|
||||||
|
if (json["encryption"]?.jsonPrimitive?.booleanOrNull == true) {
|
||||||
|
val data = json["data"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
if (data.isBlank()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, "Data file corruption",
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val passwordField = OutlinePasswordField()
|
||||||
|
val panel = object : JPanel(BorderLayout()) {
|
||||||
|
override fun requestFocusInWindow(): Boolean {
|
||||||
|
return passwordField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = JLabel("Please enter the password" + StringUtils.SPACE.repeat(25))
|
||||||
|
label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
panel.add(label, BorderLayout.NORTH)
|
||||||
|
panel.add(passwordField, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
panel,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
initialValue = passwordField
|
||||||
|
) != JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordField.password.isEmpty()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.doorman.unlock-data"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = String(passwordField.password)
|
||||||
|
val key = PBKDF2.generateSecret(
|
||||||
|
password.toCharArray(),
|
||||||
|
password.toByteArray(), keyLength = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val dataText = AES.ECB.decrypt(key, Base64.decodeBase64(data)).toString(Charsets.UTF_8)
|
||||||
|
val dataJsonResult = ohMyJson.runCatching { decodeFromString<JsonObject>(dataText) }
|
||||||
|
if (dataJsonResult.isFailure) {
|
||||||
|
val e = dataJsonResult.exceptionOrNull() ?: return
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json = dataJsonResult.getOrNull() ?: return
|
||||||
|
break
|
||||||
|
} catch (_: Exception) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.doorman.password-wrong"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ranges.contains(SyncRange.Hosts)) {
|
if (ranges.contains(SyncRange.Hosts)) {
|
||||||
val hosts = json["hosts"]
|
val hosts = json["hosts"]
|
||||||
if (hosts is JsonArray) {
|
if (hosts is JsonArray) {
|
||||||
@@ -707,6 +852,17 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ranges.contains(SyncRange.Snippets)) {
|
||||||
|
val snippets = json["snippets"]
|
||||||
|
if (snippets is JsonArray) {
|
||||||
|
ohMyJson.runCatching { decodeFromJsonElement<List<Snippet>>(snippets.jsonArray) }.onSuccess {
|
||||||
|
for (snippet in it) {
|
||||||
|
snippetManager.addSnippet(snippet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ranges.contains(SyncRange.KeyPairs)) {
|
if (ranges.contains(SyncRange.KeyPairs)) {
|
||||||
val keyPairs = json["keyPairs"]
|
val keyPairs = json["keyPairs"]
|
||||||
if (keyPairs is JsonArray) {
|
if (keyPairs is JsonArray) {
|
||||||
@@ -756,9 +912,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportText(file: File) {
|
private fun exportText(file: File, password: String) {
|
||||||
val syncConfig = getSyncConfig()
|
val syncConfig = getSyncConfig()
|
||||||
val text = ohMyJson.encodeToString(buildJsonObject {
|
var text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
put("exporter", SystemUtils.USER_NAME)
|
put("exporter", SystemUtils.USER_NAME)
|
||||||
put("version", Application.getVersion())
|
put("version", Application.getVersion())
|
||||||
@@ -768,6 +924,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
if (syncConfig.ranges.contains(SyncRange.Hosts)) {
|
||||||
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts()))
|
||||||
}
|
}
|
||||||
|
if (syncConfig.ranges.contains(SyncRange.Snippets)) {
|
||||||
|
put("snippets", ohMyJson.encodeToJsonElement(snippetManager.snippets()))
|
||||||
|
}
|
||||||
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
if (syncConfig.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs()))
|
||||||
}
|
}
|
||||||
@@ -797,6 +956,19 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
put("terminal", ohMyJson.encodeToJsonElement(database.terminal.getProperties()))
|
put("terminal", ohMyJson.encodeToJsonElement(database.terminal.getProperties()))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (password.isNotBlank()) {
|
||||||
|
val key = PBKDF2.generateSecret(
|
||||||
|
password.toCharArray(),
|
||||||
|
password.toByteArray(), keyLength = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
|
put("encryption", true)
|
||||||
|
put("data", AES.ECB.encrypt(key, text.toByteArray(Charsets.UTF_8)).encodeBase64String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
file.outputStream().use {
|
file.outputStream().use {
|
||||||
IOUtils.write(text, it, StandardCharsets.UTF_8)
|
IOUtils.write(text, it, StandardCharsets.UTF_8)
|
||||||
OptionPane.openFileInFolder(
|
OptionPane.openFileInFolder(
|
||||||
@@ -824,6 +996,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (keymapCheckBox.isSelected) {
|
if (keymapCheckBox.isSelected) {
|
||||||
range.add(SyncRange.Keymap)
|
range.add(SyncRange.Keymap)
|
||||||
}
|
}
|
||||||
|
if (snippetsCheckBox.isSelected) {
|
||||||
|
range.add(SyncRange.Snippets)
|
||||||
|
}
|
||||||
return SyncConfig(
|
return SyncConfig(
|
||||||
type = typeComboBox.selectedItem as SyncType,
|
type = typeComboBox.selectedItem as SyncType,
|
||||||
token = String(tokenTextField.password),
|
token = String(tokenTextField.password),
|
||||||
@@ -900,6 +1075,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
keymapCheckBox.isEnabled = false
|
keymapCheckBox.isEnabled = false
|
||||||
keywordHighlightsCheckBox.isEnabled = false
|
keywordHighlightsCheckBox.isEnabled = false
|
||||||
hostsCheckBox.isEnabled = false
|
hostsCheckBox.isEnabled = false
|
||||||
|
snippetsCheckBox.isEnabled = false
|
||||||
domainTextField.isEnabled = false
|
domainTextField.isEnabled = false
|
||||||
|
|
||||||
if (push) {
|
if (push) {
|
||||||
@@ -929,6 +1105,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
uploadConfigButton.isEnabled = true
|
uploadConfigButton.isEnabled = true
|
||||||
keysCheckBox.isEnabled = true
|
keysCheckBox.isEnabled = true
|
||||||
hostsCheckBox.isEnabled = true
|
hostsCheckBox.isEnabled = true
|
||||||
|
snippetsCheckBox.isEnabled = true
|
||||||
typeComboBox.isEnabled = true
|
typeComboBox.isEnabled = true
|
||||||
macrosCheckBox.isEnabled = true
|
macrosCheckBox.isEnabled = true
|
||||||
keymapCheckBox.isEnabled = true
|
keymapCheckBox.isEnabled = true
|
||||||
@@ -987,14 +1164,17 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
typeComboBox.addItem(SyncType.GitHub)
|
typeComboBox.addItem(SyncType.GitHub)
|
||||||
typeComboBox.addItem(SyncType.GitLab)
|
typeComboBox.addItem(SyncType.GitLab)
|
||||||
typeComboBox.addItem(SyncType.Gitee)
|
typeComboBox.addItem(SyncType.Gitee)
|
||||||
|
typeComboBox.addItem(SyncType.WebDAV)
|
||||||
|
|
||||||
hostsCheckBox.isFocusable = false
|
hostsCheckBox.isFocusable = false
|
||||||
|
snippetsCheckBox.isFocusable = false
|
||||||
keysCheckBox.isFocusable = false
|
keysCheckBox.isFocusable = false
|
||||||
keywordHighlightsCheckBox.isFocusable = false
|
keywordHighlightsCheckBox.isFocusable = false
|
||||||
macrosCheckBox.isFocusable = false
|
macrosCheckBox.isFocusable = false
|
||||||
keymapCheckBox.isFocusable = false
|
keymapCheckBox.isFocusable = false
|
||||||
|
|
||||||
hostsCheckBox.isSelected = sync.rangeHosts
|
hostsCheckBox.isSelected = sync.rangeHosts
|
||||||
|
snippetsCheckBox.isSelected = sync.rangeSnippets
|
||||||
keysCheckBox.isSelected = sync.rangeKeyPairs
|
keysCheckBox.isSelected = sync.rangeKeyPairs
|
||||||
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
||||||
macrosCheckBox.isSelected = sync.rangeMacros
|
macrosCheckBox.isSelected = sync.rangeMacros
|
||||||
@@ -1005,7 +1185,31 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
tokenTextField.text = sync.token
|
tokenTextField.text = sync.token
|
||||||
domainTextField.trailingComponent = JButton(Icons.externalLink).apply {
|
domainTextField.trailingComponent = JButton(Icons.externalLink).apply {
|
||||||
addActionListener {
|
addActionListener {
|
||||||
Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html"))
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
|
Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html"))
|
||||||
|
|
||||||
|
} else if (typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
|
val url = domainTextField.text
|
||||||
|
if (url.isNullOrBlank()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.settings.sync.webdav.help")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val uri = URI.create(url)
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append(uri.scheme).append("://")
|
||||||
|
if (tokenTextField.password.isNotEmpty() && gistTextField.text.isNotBlank()) {
|
||||||
|
sb.append(String(tokenTextField.password)).append(":").append(gistTextField.text)
|
||||||
|
sb.append('@')
|
||||||
|
}
|
||||||
|
sb.append(uri.authority).append(uri.path)
|
||||||
|
if (!uri.query.isNullOrBlank()) {
|
||||||
|
sb.append('?').append(uri.query)
|
||||||
|
}
|
||||||
|
Application.browse(URI.create(sb.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,12 +1219,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
tokenTextField.trailingComponent = if (tokenTextField.password.isEmpty()) getTokenBtn else null
|
tokenTextField.trailingComponent = if (tokenTextField.password.isEmpty()) getTokenBtn else null
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
if (domainTextField.text.isBlank()) {
|
||||||
if (domainTextField.text.isBlank()) {
|
if (typeComboBox.selectedItem == SyncType.GitLab) {
|
||||||
domainTextField.text = StringUtils.defaultIfBlank(sync.domain, "https://gitlab.com/api")
|
domainTextField.text = StringUtils.defaultIfBlank(sync.domain, "https://gitlab.com/api")
|
||||||
|
} else if (typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
|
domainTextField.text = sync.domain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val lastSyncTime = sync.lastSyncTime
|
val lastSyncTime = sync.lastSyncTime
|
||||||
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${
|
||||||
if (lastSyncTime > 0) DateFormatUtils.format(
|
if (lastSyncTime > 0) DateFormatUtils.format(
|
||||||
@@ -1054,7 +1261,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.layout(
|
.layout(
|
||||||
FormLayout(
|
FormLayout(
|
||||||
"left:pref, $formMargin, left:pref, $formMargin, left:pref",
|
"left:pref, $formMargin, left:pref, $formMargin, left:pref",
|
||||||
"pref, $formMargin, pref"
|
"pref, 2dlu, pref"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.add(hostsCheckBox).xy(1, 1)
|
.add(hostsCheckBox).xy(1, 1)
|
||||||
@@ -1062,6 +1269,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.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)
|
.add(keymapCheckBox).xy(3, 3)
|
||||||
|
.add(snippetsCheckBox).xy(5, 3)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
@@ -1069,17 +1277,37 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
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 || typeComboBox.selectedItem == SyncType.WebDAV) {
|
||||||
box.add(Box.createHorizontalStrut(4))
|
box.add(Box.createHorizontalStrut(4))
|
||||||
box.add(domainTextField)
|
box.add(domainTextField)
|
||||||
}
|
}
|
||||||
builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows)
|
||||||
.add(box).xy(3, rows).apply { rows += step }
|
.add(box).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
builder.add("${I18n.getString("termora.settings.sync.token")}:").xy(1, rows)
|
val isWebDAV = typeComboBox.selectedItem == SyncType.WebDAV
|
||||||
.add(tokenTextField).xy(3, rows).apply { rows += step }
|
|
||||||
.add("${I18n.getString("termora.settings.sync.gist")}:").xy(1, rows)
|
val tokenText = if (isWebDAV) {
|
||||||
.add(gistTextField).xy(3, rows).apply { rows += step }
|
I18n.getString("termora.new-host.general.username")
|
||||||
|
} else {
|
||||||
|
I18n.getString("termora.settings.sync.token")
|
||||||
|
}
|
||||||
|
|
||||||
|
val gistText = if (isWebDAV) {
|
||||||
|
I18n.getString("termora.new-host.general.password")
|
||||||
|
} else {
|
||||||
|
I18n.getString("termora.settings.sync.gist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeComboBox.selectedItem == SyncType.Gitee || isWebDAV) {
|
||||||
|
gistTextField.trailingComponent = null
|
||||||
|
} else {
|
||||||
|
gistTextField.trailingComponent = visitGistBtn
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add("${tokenText}:").xy(1, rows)
|
||||||
|
.add(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${gistText}:").xy(1, rows)
|
||||||
|
.add(if (isWebDAV) tokenTextField else gistTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows)
|
||||||
.add(rangeBox).xy(3, rows).apply { rows += step }
|
.add(rangeBox).xy(3, rows).apply { rows += step }
|
||||||
// Sync buttons
|
// Sync buttons
|
||||||
@@ -1100,6 +1328,150 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
|
||||||
|
private val editCommandField = OutlineTextField(255)
|
||||||
|
private val sftpCommandField = OutlineTextField(255)
|
||||||
|
private val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
private val browseDirectoryBtn = JButton(Icons.folder)
|
||||||
|
private val pinTabComboBox = YesOrNoComboBox()
|
||||||
|
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||||
|
private val sftp get() = database.sftp
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
editCommandField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
sftp.editCommand = editCommandField.text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
sftpCommandField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
sftp.sftpCommand = sftpCommandField.text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defaultDirectoryField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
sftp.defaultDirectory = defaultDirectoryField.text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pinTabComboBox.addItemListener(object : ItemListener {
|
||||||
|
override fun itemStateChanged(e: ItemEvent) {
|
||||||
|
if (e.stateChange != ItemEvent.SELECTED) return
|
||||||
|
sftp.pinTab = pinTabComboBox.selectedItem as Boolean
|
||||||
|
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
val evt = AnActionEvent(window, StringUtils.EMPTY, EventObject(window))
|
||||||
|
val manager = evt.getData(DataProviders.TerminalTabbedManager) ?: continue
|
||||||
|
|
||||||
|
if (sftp.pinTab) {
|
||||||
|
if (manager.getTerminalTabs().none { it is SFTPTab }) {
|
||||||
|
manager.addTerminalTab(1, SFTPTab(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新状态
|
||||||
|
manager.refreshTerminalTabs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
preserveModificationTimeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
sftp.preserveModificationTime = preserveModificationTimeComboBox.selectedItem as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
browseDirectoryBtn.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.defaultDirectory = StringUtils.defaultIfBlank(
|
||||||
|
defaultDirectoryField.text,
|
||||||
|
SystemUtils.USER_HOME
|
||||||
|
)
|
||||||
|
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
|
chooser.showOpenDialog(owner).thenAccept { files ->
|
||||||
|
if (files.isNotEmpty()) defaultDirectoryField.text = files.first().absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
editCommandField.placeholderText = "notepad {0}"
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
editCommandField.placeholderText = "open -a TextEdit {0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows) {
|
||||||
|
sftpCommandField.placeholderText = "sftp.exe"
|
||||||
|
} else {
|
||||||
|
sftpCommandField.placeholderText = "sftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultDirectoryField.placeholderText = SystemUtils.USER_HOME
|
||||||
|
defaultDirectoryField.trailingComponent = browseDirectoryBtn
|
||||||
|
|
||||||
|
defaultDirectoryField.text = sftp.defaultDirectory
|
||||||
|
editCommandField.text = sftp.editCommand
|
||||||
|
sftpCommandField.text = sftp.sftpCommand
|
||||||
|
pinTabComboBox.selectedItem = sftp.pinTab
|
||||||
|
preserveModificationTimeComboBox.selectedItem = sftp.preserveModificationTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return "SFTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $formMargin, default:grow, 30dlu",
|
||||||
|
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
val box = Box.createHorizontalBox()
|
||||||
|
box.add(JLabel("${I18n.getString("termora.settings.sftp.preserve-time")}:"))
|
||||||
|
box.add(Box.createHorizontalStrut(8))
|
||||||
|
box.add(preserveModificationTimeComboBox)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val builder = FormBuilder.create().layout(layout).debug(false)
|
||||||
|
builder.add("${I18n.getString("termora.settings.sftp.fixed-tab")}:").xy(1, rows)
|
||||||
|
builder.add(pinTabComboBox).xy(3, rows).apply { rows += 2 }
|
||||||
|
builder.add("${I18n.getString("termora.settings.sftp.edit-command")}:").xy(1, rows)
|
||||||
|
builder.add(editCommandField).xy(3, rows).apply { rows += 2 }
|
||||||
|
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, rows)
|
||||||
|
builder.add(sftpCommandField).xy(3, rows).apply { rows += 2 }
|
||||||
|
builder.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
builder.add(defaultDirectoryField).xy(3, rows).apply { rows += 2 }
|
||||||
|
builder.add(box).xyw(1, rows, 3).apply { rows += 2 }
|
||||||
|
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class AboutOption : JPanel(BorderLayout()), Option {
|
private inner class AboutOption : JPanel(BorderLayout()), Option {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -1311,13 +1683,15 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
val hosts = hostManager.hosts()
|
val hosts = hostManager.hosts()
|
||||||
val keyPairs = keyManager.getOhKeyPairs()
|
val keyPairs = keyManager.getOhKeyPairs()
|
||||||
|
val snippets = snippetManager.snippets()
|
||||||
|
|
||||||
// 获取到安全的属性,如果设置密码那表示之前并未加密
|
// 获取到安全的属性,如果设置密码那表示之前并未加密
|
||||||
// 这里取出来之后重新存储加密
|
// 这里取出来之后重新存储加密
|
||||||
val properties = database.getSafetyProperties().map { Pair(it, it.getProperties()) }
|
val properties = database.getSafetyProperties().map { Pair(it, it.getProperties()) }
|
||||||
|
|
||||||
val key = doorman.work(passwordTextField.password)
|
val key = doorman.work(passwordTextField.password)
|
||||||
|
|
||||||
hosts.forEach { hostManager.addHost(it, false) }
|
hosts.forEach { hostManager.addHost(it) }
|
||||||
|
snippets.forEach { snippetManager.addSnippet(it) }
|
||||||
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
keyPairs.forEach { keyManager.addOhKeyPair(it) }
|
||||||
for (e in properties) {
|
for (e in properties) {
|
||||||
for ((k, v) in e.second) {
|
for ((k, v) in e.second) {
|
||||||
|
|||||||
343
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
343
src/main/kotlin/app/termora/SimpleTree.kt
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import org.jdesktop.swingx.JXTree
|
||||||
|
import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
import java.awt.datatransfer.Transferable
|
||||||
|
import java.awt.datatransfer.UnsupportedFlavorException
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.event.CellEditorListener
|
||||||
|
import javax.swing.event.ChangeEvent
|
||||||
|
import javax.swing.tree.TreePath
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
open class SimpleTree : JXTree() {
|
||||||
|
|
||||||
|
protected open val model get() = super.getModel() as SimpleTreeModel<*>
|
||||||
|
private val editor = OutlineTextField(64)
|
||||||
|
protected val tree get() = this
|
||||||
|
|
||||||
|
init {
|
||||||
|
initViews()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initViews() {
|
||||||
|
|
||||||
|
|
||||||
|
// renderer
|
||||||
|
setCellRenderer(object : DefaultXTreeCellRenderer() {
|
||||||
|
override fun getTreeCellRendererComponent(
|
||||||
|
tree: JTree,
|
||||||
|
value: Any,
|
||||||
|
sel: Boolean,
|
||||||
|
expanded: Boolean,
|
||||||
|
leaf: Boolean,
|
||||||
|
row: Int,
|
||||||
|
hasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
val node = value as SimpleTreeNode<*>
|
||||||
|
val c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
||||||
|
icon = node.getIcon(sel, expanded, hasFocus)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// rename
|
||||||
|
setCellEditor(object : DefaultCellEditor(editor) {
|
||||||
|
override fun isCellEditable(e: EventObject?): Boolean {
|
||||||
|
if (e is MouseEvent || !tree.isCellEditable(e)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.isCellEditable(e).apply {
|
||||||
|
if (this) {
|
||||||
|
editor.preferredSize = Dimension(min(220, width - 64), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCellEditorValue(): Any? {
|
||||||
|
return getLastSelectedPathNode()?.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
// 右键选中
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFocusInWindow()
|
||||||
|
|
||||||
|
val selectionRows = selectionModel.selectionRows
|
||||||
|
|
||||||
|
val selRow = getClosestRowForLocation(e.x, e.y)
|
||||||
|
if (selRow < 0) {
|
||||||
|
selectionModel.clearSelection()
|
||||||
|
return
|
||||||
|
} else if (selectionRows != null && selectionRows.contains(selRow)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionPath = getPathForLocation(e.x, e.y)
|
||||||
|
|
||||||
|
setSelectionRow(selRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// contextmenu
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
if (!(SwingUtilities.isRightMouseButton(e))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(lastSelectedPathComponent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showContextmenu(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// rename
|
||||||
|
getCellEditor().addCellEditorListener(object : CellEditorListener {
|
||||||
|
override fun editingStopped(e: ChangeEvent) {
|
||||||
|
val node = getLastSelectedPathNode() ?: return
|
||||||
|
if (editor.text.isBlank() || editor.text == node.toString()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onRenamed(node, editor.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun editingCanceled(e: ChangeEvent) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// drag
|
||||||
|
transferHandler = object : TransferHandler() {
|
||||||
|
|
||||||
|
override fun createTransferable(c: JComponent): Transferable? {
|
||||||
|
val nodes = getSelectionSimpleTreeNodes().toMutableList()
|
||||||
|
if (nodes.isEmpty()) return null
|
||||||
|
if (nodes.contains(model.root)) return null
|
||||||
|
|
||||||
|
val iterator = nodes.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val node = iterator.next()
|
||||||
|
val parents = model.getPathToRoot(node).filter { it != node }
|
||||||
|
if (parents.any { nodes.contains(it) }) {
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MoveNodeTransferable(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSourceActions(c: JComponent?): Int {
|
||||||
|
return MOVE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canImport(support: TransferSupport): Boolean {
|
||||||
|
if (support.component != tree) return false
|
||||||
|
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||||
|
val path = dropLocation.path ?: return false
|
||||||
|
val node = path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||||
|
if (!support.isDataFlavorSupported(MoveNodeTransferable.dataFlavor)) return false
|
||||||
|
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||||
|
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||||
|
if (nodes.isEmpty()) return false
|
||||||
|
if (!node.isFolder) return false
|
||||||
|
|
||||||
|
for (e in nodes) {
|
||||||
|
// 禁止拖拽到自己的子下面
|
||||||
|
if (path.equals(TreePath(e.path)) || TreePath(e.path).isDescendant(path)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件夹只能拖拽到文件夹的下面
|
||||||
|
if (e.isFolder) {
|
||||||
|
if (dropLocation.childIndex > node.folderCount) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (dropLocation.childIndex != -1) {
|
||||||
|
// 非文件夹也不能拖拽到文件夹的上面
|
||||||
|
if (dropLocation.childIndex < node.folderCount) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val p = e.parent ?: continue
|
||||||
|
// 如果是同级目录排序,那么判断是不是自己的上下,如果是的话也禁止
|
||||||
|
if (p == node && dropLocation.childIndex != -1) {
|
||||||
|
val idx = p.getIndex(e)
|
||||||
|
if (dropLocation.childIndex in idx..idx + 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
support.setShowDropLocation(true)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun importData(support: TransferSupport): Boolean {
|
||||||
|
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
|
||||||
|
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
|
||||||
|
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>)
|
||||||
|
?.filterIsInstance<SimpleTreeNode<*>>() ?: return false
|
||||||
|
|
||||||
|
// 展开的 node
|
||||||
|
val expanded = mutableSetOf(node.id)
|
||||||
|
for (e in nodes) {
|
||||||
|
e.getAllChildren().filter { isExpanded(TreePath(model.getPathToRoot(it))) }
|
||||||
|
.map { it }.forEach { expanded.add(it.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转移
|
||||||
|
for (e in nodes) {
|
||||||
|
model.removeNodeFromParent(e)
|
||||||
|
rebase(e, node)
|
||||||
|
|
||||||
|
if (dropLocation.childIndex == -1) {
|
||||||
|
if (e.isFolder) {
|
||||||
|
model.insertNodeInto(e, node, node.folderCount)
|
||||||
|
} else {
|
||||||
|
model.insertNodeInto(e, node, node.childCount)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (e.isFolder) {
|
||||||
|
model.insertNodeInto(e, node, min(node.folderCount, dropLocation.childIndex))
|
||||||
|
} else {
|
||||||
|
model.insertNodeInto(e, node, min(node.childCount, dropLocation.childIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先展开最顶级的
|
||||||
|
expandPath(TreePath(model.getPathToRoot(node)))
|
||||||
|
|
||||||
|
for (child in node.getAllChildren()) {
|
||||||
|
if (expanded.contains(child.id)) {
|
||||||
|
expandPath(TreePath(model.getPathToRoot(child)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun newFolder(newNode: SimpleTreeNode<*>): Boolean {
|
||||||
|
val lastNode = lastSelectedPathComponent
|
||||||
|
if (lastNode !is SimpleTreeNode<*>) return false
|
||||||
|
return newNode(newNode, lastNode.folderCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun newFile(newNode: SimpleTreeNode<*>): Boolean {
|
||||||
|
val lastNode = lastSelectedPathComponent
|
||||||
|
if (lastNode !is SimpleTreeNode<*>) return false
|
||||||
|
return newNode(newNode, lastNode.childCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
|
||||||
|
val lastNode = lastSelectedPathComponent
|
||||||
|
if (lastNode !is SimpleTreeNode<*>) return false
|
||||||
|
model.insertNodeInto(newNode, lastNode, index)
|
||||||
|
selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
startEditingAtPath(selectionPath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getLastSelectedPathNode(): SimpleTreeNode<*>? {
|
||||||
|
return lastSelectedPathComponent as? SimpleTreeNode<*>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected open fun showContextmenu(evt: MouseEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onRenamed(node: SimpleTreeNode<*>, text: String) {}
|
||||||
|
|
||||||
|
protected open fun refreshNode(node: SimpleTreeNode<*>) {
|
||||||
|
val state = TreeUtils.saveExpansionState(tree)
|
||||||
|
val rows = selectionRows
|
||||||
|
|
||||||
|
model.reload(node)
|
||||||
|
|
||||||
|
TreeUtils.loadExpansionState(tree, state)
|
||||||
|
|
||||||
|
super.setSelectionRows(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包含孙子
|
||||||
|
*/
|
||||||
|
open fun getSelectionSimpleTreeNodes(include: Boolean = false): List<SimpleTreeNode<*>> {
|
||||||
|
val paths = selectionPaths ?: return emptyList()
|
||||||
|
if (paths.isEmpty()) return emptyList()
|
||||||
|
val nodes = mutableListOf<SimpleTreeNode<*>>()
|
||||||
|
val parents = paths.mapNotNull { it.lastPathComponent }
|
||||||
|
.filterIsInstance<SimpleTreeNode<*>>().toMutableList()
|
||||||
|
|
||||||
|
if (include) {
|
||||||
|
while (parents.isNotEmpty()) {
|
||||||
|
val node = parents.removeFirst()
|
||||||
|
nodes.add(node)
|
||||||
|
parents.addAll(node.children().toList().filterIsInstance<SimpleTreeNode<*>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (include) nodes else parents
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun isCellEditable(e: EventObject?): Boolean {
|
||||||
|
return getLastSelectedPathNode() != model.root
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MoveNodeTransferable(val nodes: List<SimpleTreeNode<*>>) : Transferable {
|
||||||
|
companion object {
|
||||||
|
val dataFlavor =
|
||||||
|
DataFlavor("${DataFlavor.javaJVMLocalObjectMimeType};class=${MoveNodeTransferable::class.java.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getTransferDataFlavors(): Array<DataFlavor> {
|
||||||
|
return arrayOf(dataFlavor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
|
||||||
|
return dataFlavor == flavor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransferData(flavor: DataFlavor?): Any {
|
||||||
|
if (flavor == dataFlavor) {
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
throw UnsupportedFlavorException(flavor)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
11
src/main/kotlin/app/termora/SimpleTreeModel.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import javax.swing.tree.DefaultTreeModel
|
||||||
|
|
||||||
|
abstract class SimpleTreeModel<T>(root: SimpleTreeNode<T>) : DefaultTreeModel(root) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun getRoot(): SimpleTreeNode<T> {
|
||||||
|
return super.getRoot() as SimpleTreeNode<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
37
src/main/kotlin/app/termora/SimpleTreeNode.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import javax.swing.Icon
|
||||||
|
import javax.swing.tree.DefaultMutableTreeNode
|
||||||
|
|
||||||
|
abstract class SimpleTreeNode<T>(data: T) : DefaultMutableTreeNode(data) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
open var data: T
|
||||||
|
get() = userObject as T
|
||||||
|
set(value) = setUserObject(value)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun getParent(): SimpleTreeNode<T>? {
|
||||||
|
return super.getParent() as SimpleTreeNode<T>?
|
||||||
|
}
|
||||||
|
|
||||||
|
open val folderCount: Int get() = 0
|
||||||
|
|
||||||
|
open fun getIcon(selected: Boolean, expanded: Boolean, hasFocus: Boolean): Icon? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
open val isFolder get() = false
|
||||||
|
|
||||||
|
abstract val id: String
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
open fun getAllChildren(): List<SimpleTreeNode<T>> {
|
||||||
|
val children = mutableListOf<SimpleTreeNode<T>>()
|
||||||
|
for (child in children()) {
|
||||||
|
val c = child as? SimpleTreeNode<T> ?: continue
|
||||||
|
children.add(c)
|
||||||
|
children.addAll(c.getAllChildren())
|
||||||
|
}
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,61 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
import app.termora.terminal.TerminalSize
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
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.channel.ClientChannelEvent
|
||||||
|
import org.apache.sshd.client.config.hosts.HostConfigEntry
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
|
||||||
|
import org.apache.sshd.client.config.hosts.KnownHostEntry
|
||||||
import org.apache.sshd.client.kex.DHGClient
|
import org.apache.sshd.client.kex.DHGClient
|
||||||
|
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier
|
||||||
|
import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor
|
||||||
|
import org.apache.sshd.client.keyverifier.ServerKeyVerifier
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.common.AttributeRepository
|
||||||
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.config.keys.KeyUtils
|
||||||
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.kex.BuiltinDHFactories
|
||||||
|
import org.apache.sshd.common.keyprovider.KeyIdentityProvider
|
||||||
import org.apache.sshd.common.util.net.SshdSocketAddress
|
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
|
||||||
|
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
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 org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Window
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
import java.net.SocketAddress
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
object SshClients {
|
object SshClients {
|
||||||
|
|
||||||
|
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(30)
|
private val timeout = Duration.ofSeconds(30)
|
||||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
@@ -58,6 +87,34 @@ object SshClients {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行一个命令
|
||||||
|
*
|
||||||
|
* @return first: exitCode , second: response
|
||||||
|
*/
|
||||||
|
fun execChannel(
|
||||||
|
session: ClientSession,
|
||||||
|
command: String
|
||||||
|
): Pair<Int, String> {
|
||||||
|
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
val channel = session.createExecChannel(command)
|
||||||
|
channel.out = baos
|
||||||
|
|
||||||
|
if (channel.open().verify(timeout).await(timeout)) {
|
||||||
|
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
IOUtils.closeQuietly(channel)
|
||||||
|
|
||||||
|
if (channel.exitStatus == null) {
|
||||||
|
return Pair(-1, baos.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(channel.exitStatus, baos.toString())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开一个会话
|
* 打开一个会话
|
||||||
*/
|
*/
|
||||||
@@ -88,7 +145,7 @@ object SshClients {
|
|||||||
val sessions = mutableListOf<ClientSession>()
|
val sessions = mutableListOf<ClientSession>()
|
||||||
for (i in 0 until jumpHosts.size) {
|
for (i in 0 until jumpHosts.size) {
|
||||||
val currentHost = jumpHosts[i]
|
val currentHost = jumpHosts[i]
|
||||||
sessions.add(doOpenSession(currentHost, client))
|
sessions.add(doOpenSession(currentHost, client, i != 0))
|
||||||
|
|
||||||
// 如果有下一跳
|
// 如果有下一跳
|
||||||
if (i < jumpHosts.size - 1) {
|
if (i < jumpHosts.size - 1) {
|
||||||
@@ -102,15 +159,35 @@ object SshClients {
|
|||||||
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
|
log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}")
|
||||||
}
|
}
|
||||||
// 映射完毕之后修改Host和端口
|
// 映射完毕之后修改Host和端口
|
||||||
jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port)
|
jumpHosts[i + 1] =
|
||||||
|
nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessions.last()
|
return sessions.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doOpenSession(host: Host, client: SshClient): ClientSession {
|
fun isMiddleware(session: ClientSession): Boolean {
|
||||||
val session = client.connect(host.username, host.host, host.port)
|
if (session is JGitClientSession) {
|
||||||
|
if (session.hostConfigEntry.properties["Middleware"]?.toBoolean() == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param middleware 如果为 true 表示是跳板
|
||||||
|
*/
|
||||||
|
private fun doOpenSession(host: Host, client: SshClient, middleware: Boolean = false): ClientSession {
|
||||||
|
val entry = HostConfigEntry()
|
||||||
|
entry.port = host.port
|
||||||
|
entry.username = host.username
|
||||||
|
entry.hostName = host.host
|
||||||
|
entry.setProperty("Middleware", middleware.toString())
|
||||||
|
|
||||||
|
val session = client.connect(entry)
|
||||||
.verify(timeout).session
|
.verify(timeout).session
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
session.addPasswordIdentity(host.authentication.password)
|
session.addPasswordIdentity(host.authentication.password)
|
||||||
@@ -123,9 +200,69 @@ object SshClients {
|
|||||||
throw SshException("Authentication failed")
|
throw SshException("Authentication failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.setAttribute(HOST_KEY, host)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openTunneling(session: ClientSession, host: Host, tunneling: Tunneling): SshdSocketAddress {
|
||||||
|
|
||||||
|
val sshdSocketAddress = if (tunneling.type == TunnelingType.Local) {
|
||||||
|
session.startLocalPortForwarding(
|
||||||
|
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||||
|
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort)
|
||||||
|
)
|
||||||
|
} else if (tunneling.type == TunnelingType.Remote) {
|
||||||
|
session.startRemotePortForwarding(
|
||||||
|
SshdSocketAddress(tunneling.sourceHost, tunneling.sourcePort),
|
||||||
|
SshdSocketAddress(tunneling.destinationHost, tunneling.destinationPort),
|
||||||
|
)
|
||||||
|
} else if (tunneling.type == TunnelingType.Dynamic) {
|
||||||
|
session.startDynamicPortForwarding(
|
||||||
|
SshdSocketAddress(
|
||||||
|
tunneling.sourceHost,
|
||||||
|
tunneling.sourcePort
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SshdSocketAddress.LOCALHOST_ADDRESS
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info(
|
||||||
|
"SSH [{}] started {} port forwarding. host: {} , port: {}",
|
||||||
|
host.name,
|
||||||
|
tunneling.name,
|
||||||
|
sshdSocketAddress.hostName,
|
||||||
|
sshdSocketAddress.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sshdSocketAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun openClient(host: Host, owner: Window): Pair<SshClient, Host> {
|
||||||
|
val client = openClient(host)
|
||||||
|
var myHost = host
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
client.userInteraction = TerminalUserInteraction(owner)
|
||||||
|
client.serverKeyVerifier = DialogServerKeyVerifier(owner)
|
||||||
|
// 弹出授权框
|
||||||
|
if (host.authentication.type == AuthenticationType.No) {
|
||||||
|
val dialog = RequestAuthenticationDialog(owner, host)
|
||||||
|
val authentication = dialog.getAuthentication()
|
||||||
|
myHost = myHost.copy(
|
||||||
|
authentication = authentication,
|
||||||
|
username = dialog.getUsername(), updateDate = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
// save
|
||||||
|
if (dialog.isRemembered()) {
|
||||||
|
HostManager.getInstance().addHost(myHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client to myHost
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开一个客户端
|
* 打开一个客户端
|
||||||
@@ -156,6 +293,11 @@ object SshClients {
|
|||||||
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/180
|
||||||
|
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||||
|
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
||||||
|
|
||||||
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)
|
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||||
@@ -185,4 +327,71 @@ object SshClients {
|
|||||||
sshClient.start()
|
sshClient.start()
|
||||||
return sshClient
|
return sshClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MyDialogServerKeyVerifier(private val owner: Window) : ServerKeyVerifier, ModifiedServerKeyAcceptor {
|
||||||
|
override fun verifyServerKey(
|
||||||
|
clientSession: ClientSession,
|
||||||
|
remoteAddress: SocketAddress,
|
||||||
|
serverKey: PublicKey
|
||||||
|
): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun acceptModifiedServerKey(
|
||||||
|
clientSession: ClientSession?,
|
||||||
|
remoteAddress: SocketAddress?,
|
||||||
|
entry: KnownHostEntry?,
|
||||||
|
expected: PublicKey?,
|
||||||
|
actual: PublicKey?
|
||||||
|
): Boolean {
|
||||||
|
val result = AtomicBoolean(false)
|
||||||
|
|
||||||
|
SwingUtilities.invokeAndWait {
|
||||||
|
result.set(
|
||||||
|
OptionPane.showConfirmDialog(
|
||||||
|
parentComponent = owner,
|
||||||
|
message = I18n.getString(
|
||||||
|
"termora.host.modified-server-key",
|
||||||
|
remoteAddress.toString().replace("/", StringUtils.EMPTY),
|
||||||
|
KeyUtils.getKeyType(expected),
|
||||||
|
KeyUtils.getFingerPrint(expected),
|
||||||
|
KeyUtils.getKeyType(actual),
|
||||||
|
KeyUtils.getFingerPrint(actual),
|
||||||
|
),
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
messageType = JOptionPane.WARNING_MESSAGE,
|
||||||
|
) == JOptionPane.OK_OPTION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DialogServerKeyVerifier(
|
||||||
|
owner: Window,
|
||||||
|
) : KnownHostsServerKeyVerifier(
|
||||||
|
MyDialogServerKeyVerifier(owner),
|
||||||
|
Paths.get(Application.getBaseDataDir().absolutePath, "known_hosts")
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
modifiedServerKeyAcceptor = delegateVerifier as ModifiedServerKeyAcceptor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateKnownHostsFile(
|
||||||
|
clientSession: ClientSession?,
|
||||||
|
remoteAddress: SocketAddress?,
|
||||||
|
serverKey: PublicKey?,
|
||||||
|
file: Path?,
|
||||||
|
knownHosts: Collection<HostEntryPair?>?
|
||||||
|
): KnownHostEntry? {
|
||||||
|
if (clientSession is JGitClientSession) {
|
||||||
|
if (SshClients.isMiddleware(clientSession)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
private val terminals = mutableListOf<Terminal>()
|
private val terminals = mutableListOf<Terminal>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(scope: WindowScope): TerminalFactory {
|
fun getInstance(): TerminalFactory {
|
||||||
return scope.getOrCreate(TerminalFactory::class) { TerminalFactory() }
|
return ApplicationScope.forApplicationScope().getOrCreate(TerminalFactory::class) { TerminalFactory() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +21,11 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
|
|
||||||
// terminal logger listener
|
// terminal logger listener
|
||||||
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
terminal.getTerminalModel().addDataListener(TerminalLoggerDataListener(terminal))
|
||||||
|
terminal.addTerminalListener(object : TerminalListener {
|
||||||
|
override fun onClose(terminal: Terminal) {
|
||||||
|
terminals.remove(terminal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
terminals.add(terminal)
|
terminals.add(terminal)
|
||||||
return terminal
|
return terminal
|
||||||
@@ -51,6 +56,11 @@ class TerminalFactory private constructor() : Disposable {
|
|||||||
return colorPalette
|
return colorPalette
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun bell() {
|
||||||
|
if (config.beep) {
|
||||||
|
super.bell()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> getData(key: DataKey<T>): T {
|
override fun <T : Any> getData(key: DataKey<T>): T {
|
||||||
|
|||||||
@@ -1,39 +1,66 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
import app.termora.highlight.KeywordHighlightPaintListener
|
import app.termora.highlight.KeywordHighlightPaintListener
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import app.termora.terminal.Terminal
|
import app.termora.terminal.Terminal
|
||||||
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
import app.termora.terminal.panel.TerminalHyperlinkPaintListener
|
||||||
import app.termora.terminal.panel.TerminalPanel
|
import app.termora.terminal.panel.TerminalPanel
|
||||||
|
import app.termora.terminal.panel.TerminalWriter
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.awt.event.ComponentListener
|
import java.awt.event.ComponentListener
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class TerminalPanelFactory {
|
class TerminalPanelFactory : Disposable {
|
||||||
private val terminalPanels = mutableListOf<TerminalPanel>()
|
private val terminalPanels = mutableListOf<TerminalPanel>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(scope: Scope): TerminalPanelFactory {
|
|
||||||
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
fun getInstance(): TerminalPanelFactory {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// repaint
|
||||||
|
Painter.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {
|
||||||
val terminalPanel = TerminalPanel(terminal, ptyConnector)
|
val writer = MyTerminalWriter(ptyConnector)
|
||||||
|
val terminalPanel = TerminalPanel(terminal, writer)
|
||||||
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
terminalPanel.addTerminalPaintListener(MultipleTerminalListener())
|
||||||
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(KeywordHighlightPaintListener.getInstance())
|
||||||
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
terminalPanel.addTerminalPaintListener(TerminalHyperlinkPaintListener.getInstance())
|
||||||
terminalPanels.add(terminalPanel)
|
|
||||||
|
Disposer.register(terminalPanel, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
removeTerminalPanel(terminalPanel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addTerminalPanel(terminalPanel)
|
||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTerminalPanels(): List<TerminalPanel> {
|
fun getTerminalPanels(): Array<TerminalPanel> {
|
||||||
return terminalPanels
|
return terminalPanels.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun repaintAll() {
|
fun repaintAll() {
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
terminalPanels.forEach { it.repaintImmediate() }
|
getTerminalPanels().forEach { it.repaintImmediate() }
|
||||||
} else {
|
} else {
|
||||||
SwingUtilities.invokeLater { repaintAll() }
|
SwingUtilities.invokeLater { repaintAll() }
|
||||||
}
|
}
|
||||||
@@ -47,4 +74,89 @@ class TerminalPanelFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeTerminalPanel(terminalPanel: TerminalPanel) {
|
||||||
|
terminalPanels.remove(terminalPanel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addTerminalPanel(terminalPanel: TerminalPanel) {
|
||||||
|
terminalPanels.add(terminalPanel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Painter : Disposable {
|
||||||
|
companion object {
|
||||||
|
fun getInstance(): Painter {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(Painter::class) { Painter() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launch {
|
||||||
|
while (coroutineScope.isActive) {
|
||||||
|
delay(500.milliseconds)
|
||||||
|
SwingUtilities.invokeLater { TerminalPanelFactory.getInstance().repaintAll() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyTerminalWriter(private val ptyConnector: PtyConnector) : TerminalWriter {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(MyTerminalWriter::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var evt: AnActionEvent
|
||||||
|
|
||||||
|
override fun onMounted(c: JComponent) {
|
||||||
|
evt = AnActionEvent(c, StringUtils.EMPTY, EventObject(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(request: TerminalWriter.WriteRequest) {
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("write: ${String(request.buffer, getCharset())}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val windowScope = evt.getData(DataProviders.WindowScope)
|
||||||
|
if (windowScope == null) {
|
||||||
|
ptyConnector.write(request.buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val multipleAction = MultipleAction.getInstance(windowScope)
|
||||||
|
if (!multipleAction.isSelected) {
|
||||||
|
ptyConnector.write(request.buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager)
|
||||||
|
if (terminalTabbedManager == null) {
|
||||||
|
ptyConnector.write(request.buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tab in terminalTabbedManager.getTerminalTabs()) {
|
||||||
|
val writer = tab.getData(DataProviders.TerminalWriter) ?: continue
|
||||||
|
if (writer is MyTerminalWriter) {
|
||||||
|
writer.ptyConnector.write(request.buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(rows: Int, cols: Int) {
|
||||||
|
ptyConnector.resize(rows, cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCharset(): Charset {
|
||||||
|
return ptyConnector.getCharset()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.actions.DataProvider
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
|
|
||||||
interface TerminalTab : Disposable {
|
interface TerminalTab : Disposable, DataProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标题
|
* 标题
|
||||||
@@ -42,6 +43,8 @@ interface TerminalTab : Disposable {
|
|||||||
*/
|
*/
|
||||||
fun canClose(): Boolean = true
|
fun canClose(): Boolean = true
|
||||||
|
|
||||||
|
fun willBeClose(): Boolean = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否可以克隆
|
* 是否可以克隆
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
|||||||
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.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
|
||||||
@@ -18,11 +17,8 @@ import java.awt.event.MouseAdapter
|
|||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.Icon
|
import javax.swing.*
|
||||||
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(
|
||||||
@@ -76,21 +72,19 @@ class TerminalTabbed(
|
|||||||
tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
|
tabbedPane.addPropertyChangeListener("selectedIndex") { evt ->
|
||||||
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) {
|
||||||
tabs[oldIndex].onLostFocus()
|
tabs[oldIndex].onLostFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabbedPane.getComponentAt(newIndex).requestFocusInWindow()
|
||||||
|
|
||||||
if (newIndex >= 0 && tabs.size > newIndex) {
|
if (newIndex >= 0 && tabs.size > newIndex) {
|
||||||
tabs[newIndex].onGrabFocus()
|
tabs[newIndex].onGrabFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择变动
|
|
||||||
tabbedPane.addChangeListener {
|
|
||||||
if (tabbedPane.selectedIndex >= 0) {
|
|
||||||
val c = tabbedPane.getComponentAt(tabbedPane.selectedIndex)
|
|
||||||
c.requestFocusInWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
tabbedPane.addMouseListener(object : MouseAdapter() {
|
tabbedPane.addMouseListener(object : MouseAdapter() {
|
||||||
@@ -126,7 +120,7 @@ class TerminalTabbed(
|
|||||||
val results = mutableListOf<FindEverywhereResult>()
|
val results = mutableListOf<FindEverywhereResult>()
|
||||||
for (i in 0 until tabbedPane.tabCount) {
|
for (i in 0 until tabbedPane.tabCount) {
|
||||||
val c = tabbedPane.getComponentAt(i)
|
val c = tabbedPane.getComponentAt(i)
|
||||||
if (c is WelcomePanel || c is TransportPanel) {
|
if (c is JComponent && c.getClientProperty(FindEverywhereProvider.SKIP_FIND_EVERYWHERE) != null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
results.add(
|
results.add(
|
||||||
@@ -160,7 +154,7 @@ class TerminalTabbed(
|
|||||||
val tab = tabs[index]
|
val tab = tabs[index]
|
||||||
|
|
||||||
if (disposable) {
|
if (disposable) {
|
||||||
if (!tab.canClose()) {
|
if (!tab.willBeClose()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,6 +171,9 @@ class TerminalTabbed(
|
|||||||
// 新的获取到焦点
|
// 新的获取到焦点
|
||||||
tabs[tabbedPane.selectedIndex].onGrabFocus()
|
tabs[tabbedPane.selectedIndex].onGrabFocus()
|
||||||
|
|
||||||
|
// 新的真正获取焦点
|
||||||
|
tabbedPane.getComponentAt(tabbedPane.selectedIndex).requestFocusInWindow()
|
||||||
|
|
||||||
if (disposable) {
|
if (disposable) {
|
||||||
Disposer.dispose(tab)
|
Disposer.dispose(tab)
|
||||||
}
|
}
|
||||||
@@ -193,12 +190,11 @@ class TerminalTabbed(
|
|||||||
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
val rename = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.rename"))
|
||||||
rename.addActionListener {
|
rename.addActionListener {
|
||||||
if (tabIndex > 0) {
|
if (tabIndex > 0) {
|
||||||
val dialog = InputDialog(
|
val text = OptionPane.showInputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
title = rename.text,
|
title = rename.text,
|
||||||
text = tabbedPane.getTitleAt(tabIndex),
|
value = tabbedPane.getTitleAt(tabIndex)
|
||||||
)
|
)
|
||||||
val text = dialog.getText()
|
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
tabbedPane.setTitleAt(tabIndex, text)
|
tabbedPane.setTitleAt(tabIndex, text)
|
||||||
c.putClientProperty(titleProperty, text)
|
c.putClientProperty(titleProperty, text)
|
||||||
@@ -217,6 +213,21 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
||||||
|
edit.addActionListener(object : AnAction() {
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
val host = hostManager.getHost(tab.host.id) ?: return
|
||||||
|
val dialog = HostDialog(evt.window, host)
|
||||||
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
|
dialog.isVisible = true
|
||||||
|
hostManager.addHost(dialog.host ?: return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 在新窗口中打开
|
// 在新窗口中打开
|
||||||
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(object : AnAction() {
|
openInNewWindow.addActionListener(object : AnAction() {
|
||||||
@@ -238,6 +249,17 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||||
|
if (openHostAction != null) {
|
||||||
|
if (tab.host.protocol == Protocol.SSH || tab.host.protocol == Protocol.SFTPPty) {
|
||||||
|
popupMenu.addSeparator()
|
||||||
|
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||||
|
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
|
|
||||||
// 关闭
|
// 关闭
|
||||||
@@ -264,9 +286,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
close.isEnabled = c !is WelcomePanel
|
close.isEnabled = tab.canClose()
|
||||||
rename.isEnabled = close.isEnabled
|
rename.isEnabled = close.isEnabled
|
||||||
clone.isEnabled = close.isEnabled
|
clone.isEnabled = close.isEnabled
|
||||||
|
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
|
||||||
openInNewWindow.isEnabled = close.isEnabled
|
openInNewWindow.isEnabled = close.isEnabled
|
||||||
|
|
||||||
// 如果不允许克隆
|
// 如果不允许克隆
|
||||||
@@ -290,7 +313,7 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun addTab(index: Int, tab: TerminalTab) {
|
private fun addTab(index: Int, tab: TerminalTab, selected: Boolean) {
|
||||||
val c = tab.getJComponent()
|
val c = tab.getJComponent()
|
||||||
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||||
|
|
||||||
@@ -301,16 +324,60 @@ class TerminalTabbed(
|
|||||||
StringUtils.EMPTY,
|
StringUtils.EMPTY,
|
||||||
index
|
index
|
||||||
)
|
)
|
||||||
c.putClientProperty(titleProperty, title)
|
|
||||||
|
|
||||||
|
// 设置标题
|
||||||
|
c.putClientProperty(titleProperty, title)
|
||||||
// 监听 icons 变化
|
// 监听 icons 变化
|
||||||
tab.addPropertyChangeListener(iconListener)
|
tab.addPropertyChangeListener(iconListener)
|
||||||
|
|
||||||
tabs.add(index, tab)
|
tabs.add(index, tab)
|
||||||
tabbedPane.selectedIndex = index
|
|
||||||
|
if (selected) {
|
||||||
|
tabbedPane.selectedIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
tabbedPane.setTabClosable(index, tab.canClose())
|
||||||
|
|
||||||
Disposer.register(this, tab)
|
Disposer.register(this, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun refreshTerminalTabs() {
|
||||||
|
for (i in 0 until tabbedPane.tabCount) {
|
||||||
|
tabbedPane.setTabClosable(i, tabs[i].canClose())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
|
||||||
|
if (!SFTPPtyTerminalTab.canSupports) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
SwingUtilities.getWindowAncestor(this),
|
||||||
|
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = tab.host
|
||||||
|
|
||||||
|
if (host.protocol == Protocol.SSH) {
|
||||||
|
val envs = tab.host.options.envs().toMutableMap()
|
||||||
|
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
|
||||||
|
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (currentDir.isNotBlank()) {
|
||||||
|
envs["CurrentDir"] = currentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
host = host.copy(
|
||||||
|
protocol = Protocol.SFTPPty, updateDate = System.currentTimeMillis(),
|
||||||
|
options = host.options.copy(env = envs.toPropertiesString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
openHostAction.actionPerformed(OpenHostActionEvent(this, host, evt))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对着 ToolBar 右键
|
* 对着 ToolBar 右键
|
||||||
*/
|
*/
|
||||||
@@ -399,12 +466,12 @@ class TerminalTabbed(
|
|||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addTerminalTab(tab: TerminalTab) {
|
override fun addTerminalTab(tab: TerminalTab, selected: Boolean) {
|
||||||
addTab(tabs.size, tab)
|
addTab(tabs.size, tab, selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addTerminalTab(index: Int, tab: TerminalTab) {
|
override fun addTerminalTab(index: Int, tab: TerminalTab, selected: Boolean) {
|
||||||
addTab(index, tab)
|
addTab(index, tab, selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSelectedTerminalTab(): TerminalTab? {
|
override fun getSelectedTerminalTab(): TerminalTab? {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
interface TerminalTabbedManager {
|
interface TerminalTabbedManager {
|
||||||
fun addTerminalTab(tab: TerminalTab)
|
fun addTerminalTab(tab: TerminalTab, selected: Boolean = true)
|
||||||
fun addTerminalTab(index: Int, tab: TerminalTab)
|
fun addTerminalTab(index: Int, tab: TerminalTab, selected: Boolean = true)
|
||||||
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)
|
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
||||||
|
fun refreshTerminalTabs()
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
import app.termora.actions.ActionManager
|
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviderSupport
|
import app.termora.actions.DataProviderSupport
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.sftp.SFTPTab
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
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.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
import java.awt.KeyboardFocusManager
|
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
|
import java.awt.event.MouseListener
|
||||||
|
import java.awt.event.MouseMotionListener
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.Box
|
import javax.swing.JComponent
|
||||||
import javax.swing.JFrame
|
import javax.swing.JFrame
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
fun assertEventDispatchThread() {
|
fun assertEventDispatchThread() {
|
||||||
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
if (!isEventDispatchThread()) throw WrongThreadException("AWT EventQueue")
|
||||||
@@ -32,17 +32,15 @@ fun assertEventDispatchThread() {
|
|||||||
class TermoraFrame : JFrame(), DataProvider {
|
class TermoraFrame : JFrame(), DataProvider {
|
||||||
|
|
||||||
|
|
||||||
private val actionManager get() = ActionManager.getInstance()
|
|
||||||
private val id = UUID.randomUUID().toString()
|
private val id = UUID.randomUUID().toString()
|
||||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||||
private val titleBar = LogicCustomTitleBar.createCustomTitleBar(this)
|
|
||||||
private val tabbedPane = MyTabbedPane()
|
private val tabbedPane = MyTabbedPane()
|
||||||
private val toolbar = TermoraToolBar(titleBar, tabbedPane)
|
private val toolbar = TermoraToolBar(windowScope, this, tabbedPane)
|
||||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane)
|
||||||
private val isWindowDecorationsSupported by lazy { JBR.isWindowDecorationsSupported() }
|
|
||||||
private val dataProviderSupport = DataProviderSupport()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
private val welcomePanel = WelcomePanel(windowScope)
|
private val welcomePanel = WelcomePanel(windowScope)
|
||||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
private val sftp get() = Database.getDatabase().sftp
|
||||||
|
private val myUI = MyFlatRootPaneUI()
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -51,44 +49,146 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
val mouseAdapter = object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
getMouseHandler()?.mouseClicked(e)
|
||||||
|
}
|
||||||
|
|
||||||
forceHitTest()
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
getMouseHandler()?.mousePressed(e)
|
||||||
|
}
|
||||||
|
|
||||||
// macos 需要判断是否全部删除
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
// 当 Tab 为 0 的时候,需要加一个边距,避开控制栏
|
val mouseLayer = getMouseLayer() ?: return
|
||||||
if (SystemInfo.isMacOS && isWindowDecorationsSupported) {
|
getMouseMotionListener()?.mouseDragged(
|
||||||
tabbedPane.addChangeListener {
|
MouseEvent(
|
||||||
tabbedPane.leadingComponent = if (tabbedPane.tabCount == 0) {
|
mouseLayer,
|
||||||
Box.createHorizontalStrut(titleBar.leftInset.toInt())
|
e.id,
|
||||||
} else {
|
e.`when`,
|
||||||
null
|
e.modifiersEx,
|
||||||
|
e.x,
|
||||||
|
e.y,
|
||||||
|
e.clickCount,
|
||||||
|
e.isPopupTrigger,
|
||||||
|
e.button
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMouseHandler(): MouseListener? {
|
||||||
|
return getHandler() as? MouseListener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMouseMotionListener(): MouseMotionListener? {
|
||||||
|
return getHandler() as? MouseMotionListener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMouseLayer(): JComponent? {
|
||||||
|
val titlePane = myUI.getTitlePane() ?: return null
|
||||||
|
val handlerField = titlePane.javaClass.getDeclaredField("mouseLayer") ?: return null
|
||||||
|
handlerField.isAccessible = true
|
||||||
|
return handlerField.get(titlePane) as? JComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHandler(): Any? {
|
||||||
|
val titlePane = myUI.getTitlePane() ?: return null
|
||||||
|
val handlerField = titlePane.javaClass.getDeclaredField("handler") ?: return null
|
||||||
|
handlerField.isAccessible = true
|
||||||
|
return handlerField.get(titlePane)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||||
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// force hit
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
if (JBR.isWindowDecorationsSupported()) {
|
||||||
|
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||||
|
val customTitleBar = JBR.getWindowDecorations().createCustomTitleBar()
|
||||||
|
customTitleBar.height = height.toFloat()
|
||||||
|
|
||||||
// 监听主题变化 需要动态修改控制栏颜色
|
val mouseAdapter = object : MouseAdapter() {
|
||||||
if (SystemInfo.isWindows && isWindowDecorationsSupported) {
|
|
||||||
ThemeManager.getInstance().addThemeChangeListener(object : ThemeChangeListener {
|
private fun hit(e: MouseEvent) {
|
||||||
override fun onChanged() {
|
if (e.source == tabbedPane) {
|
||||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
||||||
|
if (index >= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customTitleBar.forceHitTest(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseEntered(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseDragged(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseMoved(e: MouseEvent) {
|
||||||
|
hit(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
terminalTabbed.addMouseListener(mouseAdapter)
|
||||||
|
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
||||||
|
|
||||||
|
tabbedPane.addMouseListener(mouseAdapter)
|
||||||
|
tabbedPane.addMouseMotionListener(mouseAdapter)
|
||||||
|
|
||||||
|
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
||||||
|
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
||||||
|
|
||||||
|
JBR.getWindowDecorations().setCustomTitleBar(this, customTitleBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
if (isWindowDecorationsSupported) {
|
|
||||||
titleBar.height = UIManager.getInt("TabbedPane.tabHeight").toFloat()
|
// macOS 要避开左边的控制栏
|
||||||
titleBar.putProperty("controls.dark", FlatLaf.isLafDark())
|
if (SystemInfo.isMacOS) {
|
||||||
JBR.getWindowDecorations().setCustomTitleBar(this, titleBar)
|
tabbedPane.tabAreaInsets = Insets(0, 76, 0, 0)
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
// Windows 10 会有1像素误差
|
||||||
|
tabbedPane.tabAreaInsets = Insets(if (SystemInfo.isWindows_11_orLater) 1 else 2, 2, 0, 0)
|
||||||
|
} else if (SystemInfo.isLinux) {
|
||||||
|
rootPane.setUI(myUI)
|
||||||
|
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SystemInfo.isLinux) {
|
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, UIManager.getInt("TabbedPane.tabHeight"))
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICON, false)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, height)
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||||
|
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||||
|
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||||
|
rootPane.putClientProperty(
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING,
|
||||||
|
FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING_MEDIUM
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
@@ -103,82 +203,22 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
minimumSize = Dimension(640, 400)
|
minimumSize = Dimension(640, 400)
|
||||||
terminalTabbed.addTerminalTab(welcomePanel)
|
terminalTabbed.addTerminalTab(welcomePanel)
|
||||||
|
|
||||||
// macOS 要避开左边的控制栏
|
// 下一次事件循环检测是否固定 SFTP
|
||||||
if (SystemInfo.isMacOS) {
|
if (sftp.pinTab) {
|
||||||
val left = max(titleBar.leftInset.toInt(), 76)
|
SwingUtilities.invokeLater {
|
||||||
if (tabbedPane.tabCount == 0) {
|
terminalTabbed.addTerminalTab(SFTPTab(), false)
|
||||||
tabbedPane.leadingComponent = Box.createHorizontalStrut(left)
|
|
||||||
} else {
|
|
||||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Disposer.register(windowScope, terminalTabbed)
|
Disposer.register(windowScope, terminalTabbed)
|
||||||
add(terminalTabbed)
|
add(terminalTabbed, BorderLayout.CENTER)
|
||||||
|
|
||||||
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
dataProviderSupport.addData(DataProviders.TabbedPane, tabbedPane)
|
||||||
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
dataProviderSupport.addData(DataProviders.TermoraFrame, this)
|
||||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun forceHitTest() {
|
|
||||||
val mouseAdapter = object : MouseAdapter() {
|
|
||||||
|
|
||||||
private fun hit(e: MouseEvent) {
|
|
||||||
if (e.source == tabbedPane) {
|
|
||||||
val index = tabbedPane.indexAtLocation(e.x, e.y)
|
|
||||||
if (index >= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
titleBar.forceHitTest(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (e.source == toolbar.getJToolBar()) {
|
|
||||||
if (!isWindowDecorationsSupported && SwingUtilities.isLeftMouseButton(e)) {
|
|
||||||
if (JBR.isWindowMoveSupported()) {
|
|
||||||
JBR.getWindowMove().startMovingTogetherWithMouse(this@TermoraFrame, e.button)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseReleased(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseEntered(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseDragged(e: MouseEvent) {
|
|
||||||
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseMoved(e: MouseEvent) {
|
|
||||||
hit(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
terminalTabbed.addMouseListener(mouseAdapter)
|
|
||||||
terminalTabbed.addMouseMotionListener(mouseAdapter)
|
|
||||||
|
|
||||||
tabbedPane.addMouseListener(mouseAdapter)
|
|
||||||
tabbedPane.addMouseMotionListener(mouseAdapter)
|
|
||||||
|
|
||||||
toolbar.getJToolBar().addMouseListener(mouseAdapter)
|
|
||||||
toolbar.getJToolBar().addMouseMotionListener(mouseAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
return dataProviderSupport.getData(dataKey)
|
return dataProviderSupport.getData(dataKey)
|
||||||
?: terminalTabbed.getData(dataKey)
|
?: terminalTabbed.getData(dataKey)
|
||||||
@@ -199,5 +239,4 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
return id.hashCode()
|
return id.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,15 @@ package app.termora
|
|||||||
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Frame
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
import javax.swing.WindowConstants.DISPOSE_ON_CLOSE
|
import javax.swing.JFrame
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import javax.swing.UIManager
|
||||||
|
import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class TermoraFrameManager {
|
class TermoraFrameManager {
|
||||||
@@ -18,14 +24,37 @@ class TermoraFrameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val frames = mutableListOf<TermoraFrame>()
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
|
||||||
fun createWindow(): TermoraFrame {
|
fun createWindow(): TermoraFrame {
|
||||||
val frame = TermoraFrame()
|
val frame = TermoraFrame().apply { registerCloseCallback(this) }
|
||||||
registerCloseCallback(frame)
|
|
||||||
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
frame.title = if (SystemInfo.isLinux) null else Application.getName()
|
||||||
frame.defaultCloseOperation = DISPOSE_ON_CLOSE
|
frame.defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
frame.setSize(1280, 800)
|
|
||||||
frame.setLocationRelativeTo(null)
|
val rectangle = getFrameRectangle() ?: FrameRectangle(-1, -1, 1280, 800, 0)
|
||||||
return frame
|
if (rectangle.isMaximized) {
|
||||||
|
frame.setSize(1280, 800)
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
frame.extendedState = rectangle.s
|
||||||
|
} else {
|
||||||
|
// 控制最小
|
||||||
|
frame.setSize(
|
||||||
|
max(rectangle.w, UIManager.getInt("Dialog.width") - 150),
|
||||||
|
max(rectangle.h, UIManager.getInt("Dialog.height") - 100)
|
||||||
|
)
|
||||||
|
if (rectangle.x == -1 && rectangle.y == -1) {
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
} else {
|
||||||
|
frame.setLocation(max(rectangle.x, 0), max(rectangle.y, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame.apply { frames.add(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWindows(): Array<TermoraFrame> {
|
||||||
|
return frames.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -33,8 +62,16 @@ class TermoraFrameManager {
|
|||||||
window.addWindowListener(object : WindowAdapter() {
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowClosed(e: WindowEvent) {
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
|
||||||
|
// 存储位置信息
|
||||||
|
saveFrameRectangle(window)
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
frames.remove(window)
|
||||||
|
|
||||||
// dispose windowScope
|
// dispose windowScope
|
||||||
Disposer.dispose(ApplicationScope.forWindowScope(e.window))
|
val windowScope = ApplicationScope.forWindowScope(e.window)
|
||||||
|
Disposer.disposeChildren(windowScope, null)
|
||||||
|
Disposer.dispose(windowScope)
|
||||||
|
|
||||||
val windowScopes = ApplicationScope.windowScopes()
|
val windowScopes = ApplicationScope.windowScopes()
|
||||||
|
|
||||||
@@ -43,18 +80,73 @@ class TermoraFrameManager {
|
|||||||
this@TermoraFrameManager.dispose()
|
this@TermoraFrameManager.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun windowClosing(e: WindowEvent) {
|
||||||
|
if (ApplicationScope.windowScopes().size == 1) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
window,
|
||||||
|
I18n.getString("termora.quit-confirm", Application.getName()),
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun tick() {
|
||||||
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
|
val windows = getWindows()
|
||||||
|
if (windows.isEmpty()) return
|
||||||
|
for (window in windows) {
|
||||||
|
if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) {
|
||||||
|
window.extendedState = window.extendedState and JFrame.ICONIFIED.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
windows.last().toFront()
|
||||||
|
} else {
|
||||||
|
SwingUtilities.invokeLater { tick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun dispose() {
|
private fun dispose() {
|
||||||
Disposer.dispose(ApplicationScope.forApplicationScope())
|
Disposer.dispose(ApplicationScope.forApplicationScope())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Disposer.getTree().assertIsEmpty(true)
|
Disposer.getTree().assertIsEmpty(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e.message)
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveFrameRectangle(frame: TermoraFrame) {
|
||||||
|
properties.putString("TermoraFrame.x", frame.x.toString())
|
||||||
|
properties.putString("TermoraFrame.y", frame.y.toString())
|
||||||
|
properties.putString("TermoraFrame.width", frame.width.toString())
|
||||||
|
properties.putString("TermoraFrame.height", frame.height.toString())
|
||||||
|
properties.putString("TermoraFrame.extendedState", frame.extendedState.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFrameRectangle(): FrameRectangle? {
|
||||||
|
val x = properties.getString("TermoraFrame.x")?.toIntOrNull() ?: return null
|
||||||
|
val y = properties.getString("TermoraFrame.y")?.toIntOrNull() ?: return null
|
||||||
|
val w = properties.getString("TermoraFrame.width")?.toIntOrNull() ?: return null
|
||||||
|
val h = properties.getString("TermoraFrame.height")?.toIntOrNull() ?: return null
|
||||||
|
val s = properties.getString("TermoraFrame.extendedState")?.toIntOrNull() ?: return null
|
||||||
|
return FrameRectangle(x, y, w, h, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class FrameRectangle(
|
||||||
|
val x: Int, val y: Int, val w: Int, val h: Int, val s: Int
|
||||||
|
) {
|
||||||
|
val isMaximized get() = (s and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH
|
||||||
|
}
|
||||||
}
|
}
|
||||||
155
src/main/kotlin/app/termora/TermoraRestarter.kt
Normal file
155
src/main/kotlin/app/termora/TermoraRestarter.kt
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.github.hstyi.restart4j.Restarter
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Component
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
|
class TermoraRestarter {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(TermoraRestarter::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): TermoraRestarter {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(TermoraRestarter::class) { TermoraRestarter() }
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
Restarter.setProcessHandler { ProcessHandle.current().pid().toInt() }
|
||||||
|
Restarter.setExecCommandsHandler { commands ->
|
||||||
|
val pb = ProcessBuilder(commands)
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
// 去掉链接库变量
|
||||||
|
pb.environment().remove("LD_LIBRARY_PATH")
|
||||||
|
}
|
||||||
|
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD)
|
||||||
|
pb.redirectError(ProcessBuilder.Redirect.DISCARD)
|
||||||
|
pb.directory(Paths.get(System.getProperty("user.home")).toFile())
|
||||||
|
pb.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val restarting = AtomicBoolean(false)
|
||||||
|
private val isSupported get() = !restarting.get() && checkIsSupported()
|
||||||
|
private val isLinuxAppImage by lazy { System.getenv("LinuxAppImage")?.toBoolean() == true }
|
||||||
|
private val startupCommand by lazy { ProcessHandle.current().info().command().getOrNull() }
|
||||||
|
private val macOSApplicationPath by lazy {
|
||||||
|
StringUtils.removeEndIgnoreCase(
|
||||||
|
Application.getAppPath(),
|
||||||
|
"/Contents/MacOS/Termora"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restart(commands: List<String>) {
|
||||||
|
if (!isSupported) return
|
||||||
|
if (!restarting.compareAndSet(false, true)) return
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
try {
|
||||||
|
doRestart(commands)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划重启,如果当前进程支持重启,那么会询问用户是否重启。如果不支持重启,那么弹窗提示需要手动重启。
|
||||||
|
*/
|
||||||
|
fun scheduleRestart(owner: Component?, commands: List<String> = emptyList()) {
|
||||||
|
|
||||||
|
if (isSupported) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.settings.restart.message"),
|
||||||
|
I18n.getString("termora.settings.restart.title"),
|
||||||
|
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
options = arrayOf(
|
||||||
|
I18n.getString("termora.settings.restart.title"),
|
||||||
|
I18n.getString("termora.cancel")
|
||||||
|
),
|
||||||
|
initialValue = I18n.getString("termora.settings.restart.title")
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
restart(commands)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.settings.restart.message"),
|
||||||
|
I18n.getString("termora.settings.restart.title"),
|
||||||
|
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doRestart(commands: List<String>) {
|
||||||
|
|
||||||
|
if (commands.isEmpty()) {
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
Restarter.restart(arrayOf("open", "-n", macOSApplicationPath))
|
||||||
|
} else if (SystemInfo.isWindows && startupCommand != null) {
|
||||||
|
Restarter.restart(arrayOf(startupCommand))
|
||||||
|
} else if (SystemInfo.isLinux) {
|
||||||
|
if (isLinuxAppImage) {
|
||||||
|
Restarter.restart(arrayOf(System.getenv("APPIMAGE")))
|
||||||
|
} else if (startupCommand != null) {
|
||||||
|
Restarter.restart(arrayOf(startupCommand))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Restarter.restart(commands.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
window.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun checkIsSupported(): Boolean {
|
||||||
|
val appPath = Application.getAppPath()
|
||||||
|
if (appPath.isBlank() || Application.isUnknownVersion()) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Restart not supported")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows && startupCommand == null) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn("Restart not supported , ProcessHandle#info#command is null.")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
if (isLinuxAppImage) {
|
||||||
|
val appImage = System.getenv("APPIMAGE") ?: StringUtils.EMPTY
|
||||||
|
return appImage.isNotBlank() && FileUtils.getFile(appImage).exists()
|
||||||
|
}
|
||||||
|
return startupCommand != null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isMacOS) {
|
||||||
|
return Application.getAppPath().isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.actions.ActionManager
|
import app.termora.actions.*
|
||||||
import app.termora.actions.AnAction
|
|
||||||
import app.termora.actions.AnActionEvent
|
|
||||||
import app.termora.actions.SettingsAction
|
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
import app.termora.findeverywhere.FindEverywhereAction
|
||||||
|
import app.termora.snippet.SnippetAction
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jetbrains.WindowDecorations
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||||
import java.awt.Insets
|
import java.awt.Rectangle
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import javax.swing.Box
|
import javax.swing.Box
|
||||||
@@ -26,7 +24,8 @@ data class ToolBarAction(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class TermoraToolBar(
|
class TermoraToolBar(
|
||||||
private val titleBar: WindowDecorations.CustomTitleBar,
|
private val windowScope: WindowScope,
|
||||||
|
private val frame: TermoraFrame,
|
||||||
private val tabbedPane: FlatTabbedPane
|
private val tabbedPane: FlatTabbedPane
|
||||||
) {
|
) {
|
||||||
private val properties by lazy { Database.getDatabase().properties }
|
private val properties by lazy { Database.getDatabase().properties }
|
||||||
@@ -42,12 +41,13 @@ class TermoraToolBar(
|
|||||||
*/
|
*/
|
||||||
fun getAllActions(): List<ToolBarAction> {
|
fun getAllActions(): List<ToolBarAction> {
|
||||||
return listOf(
|
return listOf(
|
||||||
|
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||||
ToolBarAction(Actions.SFTP, true),
|
ToolBarAction(Actions.SFTP, true),
|
||||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||||
ToolBarAction(Actions.MACRO, true),
|
ToolBarAction(Actions.MACRO, true),
|
||||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||||
ToolBarAction(Actions.MULTIPLE, true),
|
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||||
ToolBarAction(SettingsAction.SETTING, true),
|
ToolBarAction(SettingsAction.SETTING, true),
|
||||||
)
|
)
|
||||||
@@ -109,6 +109,10 @@ class TermoraToolBar(
|
|||||||
|
|
||||||
toolbar.add(Box.createHorizontalGlue())
|
toolbar.add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||||
|
toolbar.add(Box.createHorizontalStrut(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// update btn
|
// update btn
|
||||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||||
@@ -120,8 +124,13 @@ class TermoraToolBar(
|
|||||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||||
for (action in getActions()) {
|
for (action in getActions()) {
|
||||||
if (action.visible) {
|
if (action.visible) {
|
||||||
actionManager.getAction(action.id)?.let {
|
val ac = actionManager.getAction(action.id)
|
||||||
toolbar.add(actionContainerFactory.createButton(it))
|
if (ac == null) {
|
||||||
|
if (action.id == MultipleAction.MULTIPLE) {
|
||||||
|
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toolbar.add(actionContainerFactory.createButton(ac))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,14 +155,11 @@ class TermoraToolBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun adjust() {
|
fun adjust() {
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
val left = titleBar.leftInset.toInt()
|
val rectangle =
|
||||||
if (tabbedPane.tabAreaInsets.left != left) {
|
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||||
tabbedPane.tabAreaInsets = Insets(0, left, 0, 0)
|
as? Rectangle ?: return
|
||||||
}
|
val right = rectangle.width
|
||||||
} else if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
|
||||||
|
|
||||||
val right = titleBar.rightInset.toInt()
|
|
||||||
val toolbar = this@MyToolBar
|
val toolbar = this@MyToolBar
|
||||||
for (i in 0 until toolbar.componentCount) {
|
for (i in 0 until toolbar.componentCount) {
|
||||||
val c = toolbar.getComponent(i)
|
val c = toolbar.getComponent(i)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import javax.swing.JTree
|
import javax.swing.JTree
|
||||||
import javax.swing.tree.TreeModel
|
import javax.swing.tree.TreeModel
|
||||||
import javax.swing.tree.TreeNode
|
|
||||||
|
|
||||||
object TreeUtils {
|
object TreeUtils {
|
||||||
/**
|
/**
|
||||||
@@ -31,16 +31,6 @@ object TreeUtils {
|
|||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parents(node: TreeNode): List<Any> {
|
|
||||||
val parents = mutableListOf<Any>()
|
|
||||||
var p = node.parent
|
|
||||||
while (p != null) {
|
|
||||||
parents.add(p)
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
return parents
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveExpansionState(tree: JTree): String {
|
fun saveExpansionState(tree: JTree): String {
|
||||||
val rows = mutableListOf<Int>()
|
val rows = mutableListOf<Int>()
|
||||||
for (i in 0 until tree.rowCount) {
|
for (i in 0 until tree.rowCount) {
|
||||||
@@ -63,15 +53,15 @@ object TreeUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expandAll(tree: JTree) {
|
fun saveSelectionRows(tree: JTree): String {
|
||||||
var j = tree.rowCount
|
return tree.selectionRows?.joinToString(",") ?: StringUtils.EMPTY
|
||||||
var i = 0
|
}
|
||||||
while (i < j) {
|
|
||||||
tree.expandRow(i)
|
fun loadSelectionRows(tree: JTree, state: String) {
|
||||||
i += 1
|
if (state.isBlank()) return
|
||||||
j = tree.rowCount
|
for (row in state.split(",").mapNotNull { it.toIntOrNull() }) {
|
||||||
|
tree.addSelectionRow(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import app.termora.Application.ohMyJson
|
|||||||
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
|
||||||
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.commonmark.node.BulletList
|
import org.commonmark.node.BulletList
|
||||||
import org.commonmark.node.Heading
|
import org.commonmark.node.Heading
|
||||||
import org.commonmark.node.Paragraph
|
import org.commonmark.node.Paragraph
|
||||||
@@ -59,7 +60,6 @@ class UpdaterManager private constructor() {
|
|||||||
val isSelf get() = this == self
|
val isSelf get() = this == self
|
||||||
}
|
}
|
||||||
|
|
||||||
private val properties get() = Database.getDatabase().properties
|
|
||||||
var lastVersion = LatestVersion.self
|
var lastVersion = LatestVersion.self
|
||||||
|
|
||||||
fun fetchLatestVersion(): LatestVersion {
|
fun fetchLatestVersion(): LatestVersion {
|
||||||
@@ -69,6 +69,9 @@ class UpdaterManager private constructor() {
|
|||||||
.build()
|
.build()
|
||||||
val response = Application.httpClient.newCall(request).execute()
|
val response = Application.httpClient.newCall(request).execute()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Failed to fetch latest version, response was ${response.code}")
|
||||||
|
}
|
||||||
return LatestVersion.self
|
return LatestVersion.self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +100,14 @@ class UpdaterManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
val document = parser.parse("# ${name.trim()}\n${body.trim()}")
|
val document = parser.parse(
|
||||||
|
"# 🎉 ${name.trim()} (${
|
||||||
|
DateFormatUtils.format(
|
||||||
|
publishedDate,
|
||||||
|
"yyyy-MM-dd"
|
||||||
|
)
|
||||||
|
}) \n${body.trim()}"
|
||||||
|
)
|
||||||
val renderer = HtmlRenderer.builder()
|
val renderer = HtmlRenderer.builder()
|
||||||
.attributeProviderFactory {
|
.attributeProviderFactory {
|
||||||
AttributeProvider { node, _, attributes ->
|
AttributeProvider { node, _, attributes ->
|
||||||
@@ -106,7 +116,7 @@ class UpdaterManager private constructor() {
|
|||||||
attributes["style"] = "margin: 5px 0;"
|
attributes["style"] = "margin: 5px 0;"
|
||||||
} else if (node is BulletList) {
|
} else if (node is BulletList) {
|
||||||
attributes["style"] = "margin: 0 20px;"
|
attributes["style"] = "margin: 0 20px;"
|
||||||
}else if(node is Paragraph){
|
} else if (node is Paragraph) {
|
||||||
attributes["style"] = "margin: 0;"
|
attributes["style"] = "margin: 0;"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,16 +145,4 @@ class UpdaterManager private constructor() {
|
|||||||
return LatestVersion.self
|
return LatestVersion.self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun isIgnored(version: String): Boolean {
|
|
||||||
return properties.getString("ignored.version.$version", "false").toBoolean()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ignore(version: String) {
|
|
||||||
properties.putString("ignored.version.$version", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doGetLatestVersion() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ package app.termora
|
|||||||
|
|
||||||
|
|
||||||
import app.termora.actions.*
|
import app.termora.actions.*
|
||||||
import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
|
||||||
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.terminal.DataKey
|
||||||
@@ -14,7 +13,9 @@ import com.formdev.flatlaf.extras.components.FlatTextField
|
|||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
@@ -27,11 +28,16 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
private val properties get() = Database.getDatabase().properties
|
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 = NewHostTree()
|
||||||
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()
|
private val dataProviderSupport = DataProviderSupport()
|
||||||
|
private val hostTreeModel = hostTree.model as NewHostTreeModel
|
||||||
|
private var lastFocused: Component? = null
|
||||||
|
private val filterableHostTreeModel = FilterableHostTreeModel(hostTree) {
|
||||||
|
searchTextField.text.isBlank()
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -41,6 +47,7 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
putClientProperty(FlatClientProperties.TABBED_PANE_TAB_CLOSABLE, false)
|
putClientProperty(FlatClientProperties.TABBED_PANE_TAB_CLOSABLE, false)
|
||||||
|
putClientProperty(FindEverywhereProvider.SKIP_FIND_EVERYWHERE, true)
|
||||||
|
|
||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
panel.add(createSearchPanel(), BorderLayout.NORTH)
|
panel.add(createSearchPanel(), BorderLayout.NORTH)
|
||||||
@@ -126,8 +133,6 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
})
|
})
|
||||||
hostTree.showsRootHandles = true
|
hostTree.showsRootHandles = true
|
||||||
|
|
||||||
Disposer.register(this, hostTree)
|
|
||||||
|
|
||||||
val scrollPane = JScrollPane(hostTree)
|
val scrollPane = JScrollPane(hostTree)
|
||||||
scrollPane.verticalScrollBar.maximumSize = Dimension(0, 0)
|
scrollPane.verticalScrollBar.maximumSize = Dimension(0, 0)
|
||||||
scrollPane.verticalScrollBar.preferredSize = Dimension(0, 0)
|
scrollPane.verticalScrollBar.preferredSize = Dimension(0, 0)
|
||||||
@@ -138,6 +143,11 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
panel.add(scrollPane, BorderLayout.CENTER)
|
panel.add(scrollPane, BorderLayout.CENTER)
|
||||||
panel.border = BorderFactory.createEmptyBorder(10, 0, 0, 0)
|
panel.border = BorderFactory.createEmptyBorder(10, 0, 0, 0)
|
||||||
|
|
||||||
|
hostTree.model = filterableHostTreeModel
|
||||||
|
TreeUtils.loadExpansionState(
|
||||||
|
hostTree,
|
||||||
|
properties.getString("Welcome.HostTree.state", StringUtils.EMPTY)
|
||||||
|
)
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
@@ -163,37 +173,49 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
FindEverywhereProvider.getFindEverywhereProviders(windowScope)
|
FindEverywhereProvider.getFindEverywhereProviders(windowScope).add(object : FindEverywhereProvider {
|
||||||
.add(BasicFilterFindEverywhereProvider(object : FindEverywhereProvider {
|
override fun find(pattern: String): List<FindEverywhereResult> {
|
||||||
override fun find(pattern: String): List<FindEverywhereResult> {
|
var filter = hostTreeModel.root.getAllChildren()
|
||||||
return TreeUtils.children(hostTree.model, hostTree.model.root)
|
.map { it.host }
|
||||||
.filterIsInstance<Host>()
|
.filter { it.protocol != Protocol.Folder }
|
||||||
.filter { it.protocol != Protocol.Folder }
|
|
||||||
.map { HostFindEverywhereResult(it) }
|
if (pattern.isNotBlank()) {
|
||||||
|
filter = filter.filter {
|
||||||
|
if (it.protocol == Protocol.SSH) {
|
||||||
|
it.name.contains(pattern, true) || it.host.contains(pattern, true)
|
||||||
|
} else {
|
||||||
|
it.name.contains(pattern, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun group(): String {
|
return filter.map { HostFindEverywhereResult(it) }
|
||||||
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun order(): Int {
|
override fun group(): String {
|
||||||
return Integer.MIN_VALUE + 2
|
return I18n.getString("termora.find-everywhere.groups.open-new-hosts")
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
|
override fun order(): Int {
|
||||||
|
return Integer.MIN_VALUE + 2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
filterableHostTreeModel.addFilter {
|
||||||
|
val text = searchTextField.text
|
||||||
|
val host = it.host
|
||||||
|
text.isBlank() || host.name.contains(text, true)
|
||||||
|
|| host.host.contains(text, true)
|
||||||
|
|| host.username.contains(text, true)
|
||||||
|
}
|
||||||
|
|
||||||
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
private var state = StringUtils.EMPTY
|
|
||||||
override fun changedUpdate(e: DocumentEvent) {
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
val text = searchTextField.text
|
val text = searchTextField.text
|
||||||
if (text.isBlank()) {
|
filterableHostTreeModel.refresh()
|
||||||
hostTree.setModel(hostTree.model)
|
if (text.isNotBlank()) {
|
||||||
TreeUtils.loadExpansionState(hostTree, state)
|
hostTree.expandAll()
|
||||||
state = String()
|
|
||||||
} else {
|
|
||||||
if (state.isBlank()) state = TreeUtils.saveExpansionState(hostTree)
|
|
||||||
hostTree.setModel(hostTree.searchableModel)
|
|
||||||
hostTree.searchableModel.search(text)
|
|
||||||
TreeUtils.expandAll(hostTree)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -240,12 +262,22 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun onLostFocus() {
|
||||||
hostTree.setModel(null)
|
lastFocused = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
properties.putString("WelcomeFullContent", fullContent.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
override fun onGrabFocus() {
|
||||||
|
SwingUtilities.invokeLater { lastFocused?.requestFocusInWindow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
properties.putString("WelcomeFullContent", fullContent.toString())
|
||||||
|
properties.putString("Welcome.HostTree.state", TreeUtils.saveExpansionState(hostTree))
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class HostFindEverywhereResult(val host: Host) : FindEverywhereResult {
|
||||||
|
private val showMoreInfo get() = properties.getString("HostTree.showMoreInfo", "false").toBoolean()
|
||||||
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
ActionManager.getInstance()
|
ActionManager.getInstance()
|
||||||
.getAction(OpenHostAction.OPEN_HOST)
|
.getAction(OpenHostAction.OPEN_HOST)
|
||||||
@@ -261,7 +293,18 @@ class WelcomePanel(private val windowScope: WindowScope) : JPanel(BorderLayout()
|
|||||||
return Icons.terminal
|
return Icons.terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun getText(isSelected: Boolean): String {
|
||||||
|
if (showMoreInfo) {
|
||||||
|
val color = UIManager.getColor(if (isSelected) "textHighlightText" else "textInactiveText")
|
||||||
|
val moreInfo = when (host.protocol) {
|
||||||
|
Protocol.SSH -> "${host.username}@${host.host}"
|
||||||
|
Protocol.Serial -> host.options.serialComm.port
|
||||||
|
else -> StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
if (moreInfo.isNotBlank()) {
|
||||||
|
return "<html>${host.name} <font color=rgb(${color.red},${color.green},${color.blue})>${moreInfo}</font></html>"
|
||||||
|
}
|
||||||
|
}
|
||||||
return host.name
|
return host.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import app.termora.findeverywhere.FindEverywhereAction
|
|||||||
import app.termora.highlight.KeywordHighlightAction
|
import app.termora.highlight.KeywordHighlightAction
|
||||||
import app.termora.keymgr.KeyManagerAction
|
import app.termora.keymgr.KeyManagerAction
|
||||||
import app.termora.macro.MacroAction
|
import app.termora.macro.MacroAction
|
||||||
|
import app.termora.sftp.SFTPAction
|
||||||
|
import app.termora.snippet.SnippetAction
|
||||||
import app.termora.tlog.TerminalLoggerAction
|
import app.termora.tlog.TerminalLoggerAction
|
||||||
import app.termora.transport.SFTPAction
|
|
||||||
import javax.swing.Action
|
import javax.swing.Action
|
||||||
|
|
||||||
class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||||
@@ -28,11 +29,12 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
|||||||
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
||||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||||
|
|
||||||
addAction(Actions.MULTIPLE, MultipleAction())
|
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||||
addAction(Actions.APP_UPDATE, AppUpdateAction())
|
|
||||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||||
addAction(Actions.SFTP, SFTPAction())
|
addAction(Actions.SFTP, SFTPAction())
|
||||||
|
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||||
|
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
||||||
addAction(Actions.MACRO, MacroAction())
|
addAction(Actions.MACRO, MacroAction())
|
||||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.Application.httpClient
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.sun.jna.platform.win32.Advapi32
|
||||||
|
import com.sun.jna.platform.win32.WinError
|
||||||
|
import com.sun.jna.platform.win32.WinNT
|
||||||
|
import com.sun.jna.platform.win32.WinReg
|
||||||
import io.github.g00fy2.versioncompare.Version
|
import io.github.g00fy2.versioncompare.Version
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.JXEditorPane
|
import org.jdesktop.swingx.JXEditorPane
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.io.File
|
||||||
|
import java.net.ProxySelector
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.swing.BorderFactory
|
import javax.swing.BorderFactory
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.JScrollPane
|
import javax.swing.JScrollPane
|
||||||
@@ -18,12 +32,22 @@ import kotlin.concurrent.fixedRateTimer
|
|||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
class AppUpdateAction : AnAction(
|
class AppUpdateAction private constructor() : AnAction(
|
||||||
StringUtils.EMPTY,
|
StringUtils.EMPTY,
|
||||||
Icons.ideUpdate
|
Icons.ideUpdate
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
||||||
|
private const val PKG_FILE_KEY = "pkgFile"
|
||||||
|
|
||||||
|
fun getInstance(): AppUpdateAction {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val updaterManager get() = UpdaterManager.getInstance()
|
private val updaterManager get() = UpdaterManager.getInstance()
|
||||||
|
private var isRemindMeNextTime = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
@@ -42,11 +66,16 @@ class AppUpdateAction : AnAction(
|
|||||||
initialDelay = 3.minutes.inWholeMilliseconds,
|
initialDelay = 3.minutes.inWholeMilliseconds,
|
||||||
period = 5.hours.inWholeMilliseconds, daemon = true
|
period = 5.hours.inWholeMilliseconds, daemon = true
|
||||||
) {
|
) {
|
||||||
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
if (!isRemindMeNextTime) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) { supervisorScope { launch { checkUpdate() } } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkUpdate() {
|
private suspend fun checkUpdate() {
|
||||||
|
if (Application.isUnknownVersion()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val latestVersion = updaterManager.fetchLatestVersion()
|
val latestVersion = updaterManager.fetchLatestVersion()
|
||||||
if (latestVersion.isSelf) {
|
if (latestVersion.isSelf) {
|
||||||
@@ -59,15 +88,75 @@ class AppUpdateAction : AnAction(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updaterManager.isIgnored(latestVersion.version)) {
|
try {
|
||||||
|
downloadLatestPkg(latestVersion)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) { isEnabled = true }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) {
|
||||||
|
if (SystemInfo.isLinux) return
|
||||||
|
|
||||||
|
super.putValue(PKG_FILE_KEY, null)
|
||||||
|
val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64"
|
||||||
|
val osName = if (SystemInfo.isWindows) "windows" else "osx"
|
||||||
|
val suffix = if (SystemInfo.isWindows) "exe" else "dmg"
|
||||||
|
val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}"
|
||||||
|
val asset = latestVersion.assets.find { it.name == filename } ?: return
|
||||||
|
|
||||||
|
val response = httpClient
|
||||||
|
.newBuilder()
|
||||||
|
.callTimeout(15, TimeUnit.MINUTES)
|
||||||
|
.readTimeout(15, TimeUnit.MINUTES)
|
||||||
|
.proxySelector(ProxySelector.getDefault())
|
||||||
|
.build()
|
||||||
|
.newCall(Request.Builder().url(asset.downloadUrl).build())
|
||||||
|
.execute()
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.warn("Failed to download latest version ${latestVersion.version}, response code ${response.code}")
|
||||||
|
}
|
||||||
|
IOUtils.closeQuietly(response)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
val body = response.body
|
||||||
ActionManager.getInstance()
|
val input = body?.byteStream()
|
||||||
.setEnabled(Actions.APP_UPDATE, true)
|
val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}")
|
||||||
|
val output = file.outputStream()
|
||||||
|
|
||||||
|
val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess
|
||||||
|
IOUtils.closeQuietly(input, output, body, response)
|
||||||
|
|
||||||
|
if (!downloaded) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Failed to download latest version to $filename")
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("Successfully downloaded latest version to $file")
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Swing) { setLatestPkgFile(file) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLatestPkgFile(file: File) {
|
||||||
|
putValue(PKG_FILE_KEY, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLatestPkgFile(): File? {
|
||||||
|
return getValue(PKG_FILE_KEY) as? File
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUpdateDialog() {
|
private fun showUpdateDialog() {
|
||||||
@@ -106,12 +195,62 @@ class AppUpdateAction : AnAction(
|
|||||||
if (option == JOptionPane.CANCEL_OPTION) {
|
if (option == JOptionPane.CANCEL_OPTION) {
|
||||||
return
|
return
|
||||||
} else if (option == JOptionPane.NO_OPTION) {
|
} else if (option == JOptionPane.NO_OPTION) {
|
||||||
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
|
isEnabled = false
|
||||||
updaterManager.ignore(updaterManager.lastVersion.version)
|
isRemindMeNextTime = true
|
||||||
} else if (option == JOptionPane.YES_OPTION) {
|
} else if (option == JOptionPane.YES_OPTION) {
|
||||||
ActionManager.getInstance()
|
updateSelf(lastVersion)
|
||||||
.setEnabled(Actions.APP_UPDATE, false)
|
}
|
||||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
|
}
|
||||||
|
|
||||||
|
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
|
||||||
|
val file = getLatestPkgFile()
|
||||||
|
if (SystemInfo.isLinux || file == null) {
|
||||||
|
isEnabled = false
|
||||||
|
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
|
val commands = if (SystemInfo.isMacOS) listOf("open", "-n", file.absolutePath)
|
||||||
|
// 如果安装过,那么直接静默安装和自动启动
|
||||||
|
else if (isAppInstalled()) listOf(
|
||||||
|
file.absolutePath,
|
||||||
|
"/SILENT",
|
||||||
|
"/AUTOSTART",
|
||||||
|
"/NORESTART",
|
||||||
|
"/FORCECLOSEAPPLICATIONS"
|
||||||
|
)
|
||||||
|
// 没有安装过 则打开安装向导
|
||||||
|
else listOf(file.absolutePath)
|
||||||
|
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("restart {}", commands.joinToString(StringUtils.SPACE))
|
||||||
|
}
|
||||||
|
|
||||||
|
TermoraRestarter.getInstance().scheduleRestart(owner, commands)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAppInstalled(): Boolean {
|
||||||
|
val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1"
|
||||||
|
val phkKey = WinReg.HKEYByReference()
|
||||||
|
|
||||||
|
// 尝试打开注册表键
|
||||||
|
val result = Advapi32.INSTANCE.RegOpenKeyEx(
|
||||||
|
WinReg.HKEY_LOCAL_MACHINE,
|
||||||
|
keyPath,
|
||||||
|
0,
|
||||||
|
WinNT.KEY_READ,
|
||||||
|
phkKey
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result == WinError.ERROR_SUCCESS) {
|
||||||
|
// 键存在,关闭句柄
|
||||||
|
Advapi32.INSTANCE.RegCloseKey(phkKey.getValue())
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// 键不存在或无权限
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,5 +17,5 @@ interface DataProvider {
|
|||||||
/**
|
/**
|
||||||
* 数据提供
|
* 数据提供
|
||||||
*/
|
*/
|
||||||
fun <T : Any> getData(dataKey: DataKey<T>): T?
|
fun <T : Any> getData(dataKey: DataKey<T>): T? = null
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import app.termora.terminal.DataKey
|
|||||||
object DataProviders {
|
object DataProviders {
|
||||||
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
|
val TerminalPanel = DataKey(app.termora.terminal.panel.TerminalPanel::class)
|
||||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||||
val PtyConnector = DataKey(app.termora.terminal.PtyConnector::class)
|
val TerminalWriter get() = DataKey.TerminalWriter
|
||||||
|
|
||||||
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||||
@@ -17,6 +17,6 @@ object DataProviders {
|
|||||||
|
|
||||||
|
|
||||||
object Welcome {
|
object Welcome {
|
||||||
val HostTree = DataKey(app.termora.HostTree::class)
|
val HostTree = DataKey(app.termora.NewHostTree::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,32 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.I18n
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.TerminalPanelFactory
|
||||||
|
import app.termora.WindowScope
|
||||||
|
|
||||||
class MultipleAction : AnAction(
|
class MultipleAction private constructor() : AnAction(
|
||||||
I18n.getString("termora.tools.multiple"),
|
I18n.getString("termora.tools.multiple"),
|
||||||
Icons.vcs
|
Icons.vcs
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将命令发送到多个会话
|
||||||
|
*/
|
||||||
|
const val MULTIPLE = "MultipleAction"
|
||||||
|
|
||||||
|
fun getInstance(windowScope: WindowScope): MultipleAction {
|
||||||
|
return windowScope.getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setStateAction()
|
setStateAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
ApplicationScope.windowScopes().map { TerminalPanelFactory.getInstance(it) }
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
.forEach { it.repaintAll() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.Host
|
import app.termora.*
|
||||||
import app.termora.HostDialog
|
|
||||||
import app.termora.HostManager
|
|
||||||
import app.termora.Protocol
|
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
class NewHostAction : AnAction() {
|
class NewHostAction : AnAction() {
|
||||||
@@ -20,27 +17,27 @@ class NewHostAction : AnAction() {
|
|||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
||||||
val model = tree.model
|
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
|
||||||
var lastHost = tree.lastSelectedPathComponent ?: model.root
|
if (lastNode.host.protocol != Protocol.Folder) {
|
||||||
if (lastHost !is Host) {
|
lastNode = lastNode.parent ?: return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastHost.protocol != Protocol.Folder) {
|
|
||||||
val p = model.getParent(lastHost) ?: return
|
|
||||||
lastHost = p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val lastHost = lastNode.host
|
||||||
val dialog = HostDialog(evt.window)
|
val dialog = HostDialog(evt.window)
|
||||||
dialog.setLocationRelativeTo(evt.window)
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
val host = (dialog.host ?: return).copy(parentId = lastHost.id)
|
||||||
|
|
||||||
hostManager.addHost(host)
|
hostManager.addHost(host)
|
||||||
|
val newNode = HostTreeNode(host)
|
||||||
|
|
||||||
tree.expandNode(lastHost)
|
val model = if (tree.model is FilterableHostTreeModel) (tree.model as FilterableHostTreeModel).getModel()
|
||||||
|
else tree.model
|
||||||
|
|
||||||
tree.selectionPath = TreePath(model.getPathToRoot(host))
|
if (model is NewHostTreeModel) {
|
||||||
|
model.insertNodeInto(newNode, lastNode, lastNode.childCount)
|
||||||
|
tree.selectionPath = TreePath(model.getPathToRoot(newNode))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,9 +15,17 @@ class OpenHostAction : AnAction() {
|
|||||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||||
|
|
||||||
|
// 如果不支持 SFTP 那么不处理这个响应
|
||||||
|
if (evt.host.protocol == Protocol.SFTPPty) {
|
||||||
|
if (!SFTPPtyTerminalTab.canSupports) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val tab = when (evt.host.protocol) {
|
val tab = when (evt.host.protocol) {
|
||||||
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||||
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||||
|
Protocol.SFTPPty -> SFTPPtyTerminalTab(windowScope, evt.host)
|
||||||
else -> LocalTerminalTab(windowScope, evt.host)
|
else -> LocalTerminalTab(windowScope, evt.host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class OpenLocalTerminalAction : AnAction(
|
|||||||
OpenHostActionEvent(
|
OpenHostActionEvent(
|
||||||
evt.source,
|
evt.source,
|
||||||
Host(
|
Host(
|
||||||
|
id = "local",
|
||||||
name = name,
|
name = name,
|
||||||
protocol = Protocol.Local
|
protocol = Protocol.Local
|
||||||
),
|
),
|
||||||
|
|||||||
30
src/main/kotlin/app/termora/actions/SFTPCommandAction.kt
Normal file
30
src/main/kotlin/app/termora/actions/SFTPCommandAction.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.HostTerminalTab
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.OpenHostActionEvent
|
||||||
|
import app.termora.Protocol
|
||||||
|
|
||||||
|
class SFTPCommandAction : AnAction() {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 打开 SFTP command
|
||||||
|
*/
|
||||||
|
const val SFTP_COMMAND = "SFTPCommandAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
putValue(ACTION_COMMAND_KEY, SFTP_COMMAND)
|
||||||
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.open-sftp-command"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
val actionManager = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST) ?: return
|
||||||
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val tab = terminalTabbedManager.getSelectedTerminalTab() as? HostTerminalTab ?: return
|
||||||
|
val host = tab.host
|
||||||
|
if (!(host.protocol == Protocol.SSH || host.protocol == Protocol.SFTPPty)) return
|
||||||
|
actionManager.actionPerformed(OpenHostActionEvent(evt.source, host.copy(protocol = Protocol.SFTPPty), evt))
|
||||||
|
evt.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
|
||||||
class TerminalClearScreenAction : AnAction() {
|
class TerminalClearScreenAction : AnAction() {
|
||||||
companion object {
|
companion object {
|
||||||
const val CLEAR_SCREEN = "ClearScreen"
|
const val CLEAR_SCREEN = "ClearScreen"
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
putValue(SHORT_DESCRIPTION, "Clear Terminal Buffer")
|
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.clear-screen"))
|
||||||
putValue(ACTION_COMMAND_KEY, CLEAR_SCREEN)
|
putValue(ACTION_COMMAND_KEY, CLEAR_SCREEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class TerminalCopyAction : AnAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
systemClipboard.setContents(StringSelection(text), null)
|
systemClipboard.setContents(StringSelection(text), null)
|
||||||
terminalPanel.toast(I18n.getString("termora.terminal.copied"))
|
|
||||||
if (log.isTraceEnabled) {
|
if (log.isTraceEnabled) {
|
||||||
log.trace("Copy to clipboard. {}", text)
|
log.trace("Copy to clipboard. {}", text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
import app.termora.TerminalPanelFactory
|
import app.termora.TerminalPanelFactory
|
||||||
|
|
||||||
@@ -13,10 +12,8 @@ abstract class TerminalZoomAction : AnAction() {
|
|||||||
evt.getData(DataProviders.TerminalPanel) ?: return
|
evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
|
||||||
if (zoom()) {
|
if (zoom()) {
|
||||||
ApplicationScope.windowScopes().forEach {
|
TerminalPanelFactory.getInstance()
|
||||||
TerminalPanelFactory.getInstance(it)
|
|
||||||
.fireResize()
|
.fireResize()
|
||||||
}
|
|
||||||
evt.consume()
|
evt.consume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.jdesktop.swingx.action.AbstractActionExt
|
|||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import javax.swing.Action
|
import javax.swing.Action
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
open class ActionFindEverywhereResult(private val action: Action) : FindEverywhereResult {
|
open class ActionFindEverywhereResult(private val action: Action) : FindEverywhereResult {
|
||||||
private val isState: Boolean
|
private val isState: Boolean
|
||||||
@@ -26,7 +27,7 @@ open class ActionFindEverywhereResult(private val action: Action) : FindEverywhe
|
|||||||
if (isState) {
|
if (isState) {
|
||||||
action.putValue(Action.SELECTED_KEY, !isSelected)
|
action.putValue(Action.SELECTED_KEY, !isSelected)
|
||||||
}
|
}
|
||||||
action.actionPerformed(e)
|
SwingUtilities.invokeLater { action.actionPerformed(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ package app.termora.findeverywhere
|
|||||||
import app.termora.DialogWrapper
|
import app.termora.DialogWrapper
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
|
import app.termora.WindowScope
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.macro.MacroFindEverywhereProvider
|
import app.termora.macro.MacroFindEverywhereProvider
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
import com.jetbrains.JBR
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
@@ -18,7 +17,7 @@ import javax.swing.*
|
|||||||
import javax.swing.event.DocumentEvent
|
import javax.swing.event.DocumentEvent
|
||||||
import javax.swing.event.DocumentListener
|
import javax.swing.event.DocumentListener
|
||||||
|
|
||||||
class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
class FindEverywhere(owner: Window, windowScope: WindowScope) : DialogWrapper(owner) {
|
||||||
private val searchTextField = FlatTextField()
|
private val searchTextField = FlatTextField()
|
||||||
private val model = DefaultListModel<FindEverywhereResult>()
|
private val model = DefaultListModel<FindEverywhereResult>()
|
||||||
private val resultList = FindEverywhereXList(model)
|
private val resultList = FindEverywhereXList(model)
|
||||||
@@ -26,7 +25,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
private val providers = mutableListOf<FindEverywhereProvider>(
|
private val providers = mutableListOf<FindEverywhereProvider>(
|
||||||
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(QuickCommandFindEverywhereProvider()),
|
||||||
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(SettingsFindEverywhereProvider()),
|
||||||
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(QuickActionsFindEverywhereProvider(windowScope)),
|
||||||
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
|
BasicFilterFindEverywhereProvider(MacroFindEverywhereProvider()),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,17 +43,10 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
minimumSize = Dimension(size.width / 2, size.height / 2)
|
minimumSize = Dimension(size.width / 2, size.height / 2)
|
||||||
isModal = false
|
isModal = false
|
||||||
lostFocusDispose = true
|
lostFocusDispose = true
|
||||||
controlsVisible = false
|
|
||||||
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
|
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
// 不支持装饰,铺满
|
|
||||||
if (!JBR.isWindowDecorationsSupported()) {
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
|
||||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
rootPane.background = DynamicColor("desktop")
|
val desktopBackground = DynamicColor("desktop")
|
||||||
centerPanel.background = DynamicColor("desktop")
|
centerPanel.background = DynamicColor("desktop")
|
||||||
centerPanel.border = BorderFactory.createEmptyBorder(12, 12, 12, 12)
|
centerPanel.border = BorderFactory.createEmptyBorder(12, 12, 12, 12)
|
||||||
|
|
||||||
@@ -69,7 +61,7 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
resultList.isRolloverEnabled = false
|
resultList.isRolloverEnabled = false
|
||||||
resultList.selectionMode = ListSelectionModel.SINGLE_SELECTION
|
resultList.selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||||
resultList.border = BorderFactory.createEmptyBorder(5, 0, 0, 0)
|
resultList.border = BorderFactory.createEmptyBorder(5, 0, 0, 0)
|
||||||
resultList.background = rootPane.background
|
resultList.background = desktopBackground
|
||||||
|
|
||||||
|
|
||||||
val scrollPane = JScrollPane(resultList)
|
val scrollPane = JScrollPane(resultList)
|
||||||
@@ -225,5 +217,11 @@ class FindEverywhere(owner: Window) : DialogWrapper(owner) {
|
|||||||
super.setVisible(visible)
|
super.setVisible(visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addNotify() {
|
||||||
|
super.addNotify()
|
||||||
|
|
||||||
|
controlsVisible = false
|
||||||
|
fullWindowContent = true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class FindEverywhereAction : AnAction(StringUtils.EMPTY, Icons.find) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = FindEverywhere(owner)
|
val dialog = FindEverywhere(owner, scope)
|
||||||
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
|
for (provider in FindEverywhereProvider.getFindEverywhereProviders(scope)) {
|
||||||
dialog.registerProvider(provider)
|
dialog.registerProvider(provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import app.termora.Scope
|
|||||||
interface FindEverywhereProvider {
|
interface FindEverywhereProvider {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val SKIP_FIND_EVERYWHERE = "SKIP_FIND_EVERYWHERE"
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
|
fun getFindEverywhereProviders(scope: Scope): MutableList<FindEverywhereProvider> {
|
||||||
var list = scope.getAnyOrNull("FindEverywhereProviders")
|
var list = scope.getAnyOrNull("FindEverywhereProviders")
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ interface FindEverywhereResult : ActionListener {
|
|||||||
|
|
||||||
fun getIcon(isSelected: Boolean): Icon = Icons.empty
|
fun getIcon(isSelected: Boolean): Icon = Icons.empty
|
||||||
|
|
||||||
|
fun getText(isSelected: Boolean) = toString()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -94,16 +94,16 @@ class FindEverywhereXList(private val model: DefaultListModel<FindEverywhereResu
|
|||||||
label.font = font.deriveFont(font.size - 2f)
|
label.font = font.deriveFont(font.size - 2f)
|
||||||
val box = Box.createHorizontalBox()
|
val box = Box.createHorizontalBox()
|
||||||
box.add(label)
|
box.add(label)
|
||||||
/*box.add(object : JComponent() {
|
|
||||||
override fun paintComponent(g: Graphics) {
|
|
||||||
g.color = DynamicColor.BorderColor
|
|
||||||
g.drawLine(10, height / 2, width, height / 2)
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
return box
|
return box
|
||||||
}
|
}
|
||||||
|
|
||||||
val c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
val c = super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
if (value is FindEverywhereResult) value.getText(isSelected) else value,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
background = UIManager.getColor("List.selectionBackground")
|
background = UIManager.getColor("List.selectionBackground")
|
||||||
foreground = UIManager.getColor("List.selectionForeground")
|
foreground = UIManager.getColor("List.selectionForeground")
|
||||||
|
|||||||
@@ -2,21 +2,32 @@ package app.termora.findeverywhere
|
|||||||
|
|
||||||
import app.termora.Actions
|
import app.termora.Actions
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import app.termora.actions.MultipleAction
|
||||||
|
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
|
|
||||||
class QuickActionsFindEverywhereProvider : FindEverywhereProvider {
|
class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) : FindEverywhereProvider {
|
||||||
private val actions = listOf(
|
private val actions = listOf(
|
||||||
Actions.KEY_MANAGER,
|
Actions.KEY_MANAGER,
|
||||||
Actions.KEYWORD_HIGHLIGHT,
|
Actions.KEYWORD_HIGHLIGHT,
|
||||||
Actions.MULTIPLE,
|
MultipleAction.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
|
val results = ArrayList<FindEverywhereResult>()
|
||||||
.mapNotNull { actionManager.getAction(it) }
|
for (action in actions) {
|
||||||
.map { ActionFindEverywhereResult(it) }
|
val ac = actionManager.getAction(action)
|
||||||
|
if (ac == null) {
|
||||||
|
if (action == MultipleAction.MULTIPLE) {
|
||||||
|
results.add(ActionFindEverywhereResult(MultipleAction.getInstance(windowScope)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(ActionFindEverywhereResult(ac))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import app.termora.I18n
|
|||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.actions.NewHostAction
|
import app.termora.actions.NewHostAction
|
||||||
import app.termora.actions.OpenLocalTerminalAction
|
import app.termora.actions.OpenLocalTerminalAction
|
||||||
|
import app.termora.snippet.SnippetAction
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
@@ -21,6 +22,11 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
|||||||
list.add(ActionFindEverywhereResult(it))
|
list.add(ActionFindEverywhereResult(it))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snippet
|
||||||
|
actionManager.getAction(SnippetAction.SNIPPET)?.let {
|
||||||
|
list.add(ActionFindEverywhereResult(it))
|
||||||
|
}
|
||||||
|
|
||||||
// SFTP
|
// SFTP
|
||||||
actionManager.getAction(Actions.SFTP)?.let {
|
actionManager.getAction(Actions.SFTP)?.let {
|
||||||
list.add(ActionFindEverywhereResult(it))
|
list.add(ActionFindEverywhereResult(it))
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
@@ -31,7 +30,7 @@ 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.getInstance(ApplicationScope.forWindowScope(this))
|
val colorPalette = TerminalFactory.getInstance()
|
||||||
.createTerminal().getTerminalModel().getColorPalette()
|
.createTerminal().getTerminalModel().getColorPalette()
|
||||||
for (i in 1..16) {
|
for (i in 1..16) {
|
||||||
val c = JPanel()
|
val c = JPanel()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class KeywordHighlightDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
private val table = FlatTable()
|
private val table = FlatTable()
|
||||||
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
private val keywordHighlightManager by lazy { KeywordHighlightManager.getInstance() }
|
||||||
private val colorPalette by lazy {
|
private val colorPalette by lazy {
|
||||||
TerminalFactory.getInstance(ApplicationScope.forWindowScope(this)).createTerminal().getTerminalModel()
|
TerminalFactory.getInstance().createTerminal().getTerminalModel()
|
||||||
.getColorPalette()
|
.getColorPalette()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package app.termora.highlight
|
package app.termora.highlight
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.TerminalPanelFactory
|
|
||||||
import app.termora.Database
|
import app.termora.Database
|
||||||
|
import app.termora.TerminalPanelFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class KeywordHighlightManager private constructor() {
|
class KeywordHighlightManager private constructor() {
|
||||||
@@ -27,7 +27,7 @@ class KeywordHighlightManager private constructor() {
|
|||||||
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
|
fun addKeywordHighlight(keywordHighlight: KeywordHighlight) {
|
||||||
database.addKeywordHighlight(keywordHighlight)
|
database.addKeywordHighlight(keywordHighlight)
|
||||||
keywordHighlights[keywordHighlight.id] = keywordHighlight
|
keywordHighlights[keywordHighlight.id] = keywordHighlight
|
||||||
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Keyword highlighter added. {}", keywordHighlight)
|
log.debug("Keyword highlighter added. {}", keywordHighlight)
|
||||||
@@ -37,7 +37,7 @@ class KeywordHighlightManager private constructor() {
|
|||||||
fun removeKeywordHighlight(id: String) {
|
fun removeKeywordHighlight(id: String) {
|
||||||
database.removeKeywordHighlight(id)
|
database.removeKeywordHighlight(id)
|
||||||
keywordHighlights.remove(id)
|
keywordHighlights.remove(id)
|
||||||
ApplicationScope.windowScopes().forEach { TerminalPanelFactory.getInstance(it).repaintAll() }
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Keyword highlighter removed. {}", id)
|
log.debug("Keyword highlighter removed. {}", id)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TerminalUserInteraction(
|
|||||||
prompt[i],
|
prompt[i],
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.title = instruction ?: name ?: StringUtils.EMPTY
|
dialog.title = instruction ?: name ?: StringUtils.EMPTY
|
||||||
passwords[i] = dialog.getText()
|
passwords[i] = dialog.getText()
|
||||||
if (passwords[i].isBlank()) {
|
if (passwords[i].isBlank()) {
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ class KeymapImpl(private val menuShortcutKeyMaskEx: Int) : Keymap("Keymap", null
|
|||||||
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_R, menuShortcutKeyMaskEx or InputEvent.SHIFT_DOWN_MASK))
|
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_R, menuShortcutKeyMaskEx or InputEvent.SHIFT_DOWN_MASK))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Command + Shift + P
|
||||||
|
addShortcut(
|
||||||
|
SFTPCommandAction.SFTP_COMMAND,
|
||||||
|
KeyShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_P, menuShortcutKeyMaskEx or InputEvent.SHIFT_DOWN_MASK))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
// switch map
|
// switch map
|
||||||
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
|
for (i in KeyEvent.VK_1..KeyEvent.VK_9) {
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
package app.termora.keymap
|
package app.termora.keymap
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.*
|
||||||
import app.termora.Database
|
|
||||||
import app.termora.DialogWrapper
|
|
||||||
import app.termora.Disposable
|
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.action.ActionManager
|
import org.jdesktop.swingx.action.ActionManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Container
|
||||||
import java.awt.KeyEventDispatcher
|
import java.awt.KeyEventDispatcher
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JDialog
|
import javax.swing.JDialog
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
class KeymapManager private constructor() : Disposable {
|
class KeymapManager private constructor() : Disposable {
|
||||||
@@ -30,15 +27,14 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
||||||
private val myKeyEventDispatcher = MyKeyEventDispatcher()
|
|
||||||
private val database get() = Database.getDatabase()
|
private val database get() = Database.getDatabase()
|
||||||
|
private val properties get() = database.properties
|
||||||
private val keymaps = linkedMapOf<String, Keymap>()
|
private val keymaps = linkedMapOf<String, Keymap>()
|
||||||
private val activeKeymap get() = database.properties.getString("Keymap.Active")
|
private val activeKeymap get() = properties.getString("Keymap.Active")
|
||||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
|
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||||
keyboardFocusManager.addKeyEventDispatcher(myKeyEventDispatcher)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (keymap in database.getKeymaps()) {
|
for (keymap in database.getKeymaps()) {
|
||||||
@@ -128,6 +124,17 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果当前有 Popup ,那么不派发事件
|
||||||
|
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
|
if (c is Container) {
|
||||||
|
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
|
||||||
|
JPopupMenu::class.java,
|
||||||
|
c, true
|
||||||
|
)
|
||||||
|
if (popups.isNotEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
||||||
for (actionId in actionIds) {
|
for (actionId in actionIds) {
|
||||||
@@ -146,35 +153,7 @@ class KeymapManager private constructor() : Disposable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyKeyEventDispatcher : KeyEventDispatcher {
|
|
||||||
// double shift
|
|
||||||
private var lastTime = -1L
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
|
||||||
if (e.keyCode == KeyEvent.VK_SHIFT && e.id == KeyEvent.KEY_PRESSED) {
|
|
||||||
val owner = AnActionEvent(e.source, StringUtils.EMPTY, e).getData(DataProviders.TermoraFrame)
|
|
||||||
?: return false
|
|
||||||
if (keyboardFocusManager.focusedWindow == owner) {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
if (now - 250 < lastTime) {
|
|
||||||
app.termora.actions.ActionManager.getInstance()
|
|
||||||
.getAction(FindEverywhereAction.FIND_EVERYWHERE)
|
|
||||||
?.actionPerformed(AnActionEvent(e.source, StringUtils.EMPTY, e))
|
|
||||||
}
|
|
||||||
lastTime = now
|
|
||||||
}
|
|
||||||
} else if (e.keyCode != KeyEvent.VK_SHIFT) { // 如果不是 Shift 键,那么就阻断了连续性,重置时间
|
|
||||||
lastTime = -1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
|
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||||
keyboardFocusManager.removeKeyEventDispatcher(myKeyEventDispatcher)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,10 +119,11 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
|||||||
val keymap = getCurrentKeymap()
|
val keymap = getCurrentKeymap()
|
||||||
val index = keymapComboBox.selectedIndex
|
val index = keymapComboBox.selectedIndex
|
||||||
if (keymap != null && !keymap.isReadonly && index >= 0) {
|
if (keymap != null && !keymap.isReadonly && index >= 0) {
|
||||||
val text = InputDialog(
|
val text = OptionPane.showInputDialog(
|
||||||
SwingUtilities.getWindowAncestor(this@KeymapPanel),
|
SwingUtilities.getWindowAncestor(this@KeymapPanel),
|
||||||
title = renameBtn.toolTipText, text = keymap.name
|
title = renameBtn.toolTipText,
|
||||||
).getText()
|
value = keymap.name
|
||||||
|
)
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
if (text != keymap.name) {
|
if (text != keymap.name) {
|
||||||
keymapManager.removeKeymap(keymap.name)
|
keymapManager.removeKeymap(keymap.name)
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class KeymapTableModel : DefaultTableModel() {
|
|||||||
FindEverywhereAction.FIND_EVERYWHERE,
|
FindEverywhereAction.FIND_EVERYWHERE,
|
||||||
NewWindowAction.NEW_WINDOW,
|
NewWindowAction.NEW_WINDOW,
|
||||||
TabReconnectAction.RECONNECT_TAB,
|
TabReconnectAction.RECONNECT_TAB,
|
||||||
|
TerminalClearScreenAction.CLEAR_SCREEN,
|
||||||
|
SFTPCommandAction.SFTP_COMMAND,
|
||||||
SwitchTabAction.SWITCH_TAB,
|
SwitchTabAction.SWITCH_TAB,
|
||||||
)) {
|
)) {
|
||||||
val action = actionManager.getAction(id) ?: continue
|
val action = actionManager.getAction(id) ?: continue
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user