mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
IJI-2360 https://github.com/JetBrains/jewel is merged
GitOrigin-RevId: ee785825e2070b33d9981932340eee4972035e3f
This commit is contained in:
49
platform/jewel/.editorconfig
Normal file
49
platform/jewel/.editorconfig
Normal file
@@ -0,0 +1,49 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 150
|
||||
tab_width = 4
|
||||
|
||||
[{*.html,*.js,*.css}]
|
||||
indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[{*.kt,*.kts}]
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_function_signature_body_expression_wrapping = multiline
|
||||
ktlint_ignore_back_ticked_identifier = true
|
||||
ktlint_standard_annotation = disabled
|
||||
ktlint_standard_chain-method-continuation = disabled
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_condition-wrapping = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-literal = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_import-ordering = disabled
|
||||
ktlint_standard_indent = disabled
|
||||
ktlint_standard_multiline-expression-wrapping = disabled
|
||||
ktlint_standard_parameter-list-wrapping = disabled
|
||||
ktlint_standard_string-template-indent = disabled
|
||||
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||
ktlint_standard_try-catch-finally-spacing = disabled
|
||||
ktlint_standard_wrapping = disabled
|
||||
|
||||
[gradlew.bat]
|
||||
end_of_line = crlf
|
||||
|
||||
[{*.bash,*.sh,*.zsh}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
||||
[*.md]
|
||||
indent_size = 2
|
||||
34
platform/jewel/.github/workflows/build.yml
vendored
Normal file
34
platform/jewel/.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: CI checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: zulu
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run :check task
|
||||
run: ./gradlew check --continue --no-daemon
|
||||
|
||||
# - uses: github/codeql-action/upload-sarif@v3
|
||||
# if: ${{ always() }}
|
||||
# with:
|
||||
# sarif_file: ${{ github.workspace }}/build/reports/static-analysis.sarif
|
||||
# checkout_path: ${{ github.workspace }}
|
||||
36
platform/jewel/.github/workflows/check-ide-version.yml
vendored
Normal file
36
platform/jewel/.github/workflows/check-ide-version.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: IJ Platform version updates
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 8 * * *
|
||||
|
||||
jobs:
|
||||
check-ij-platform-updates:
|
||||
name: Check for IJP updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch-name:
|
||||
- releases/241
|
||||
- releases/242
|
||||
- main
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.branch-name }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 21
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run Gradle
|
||||
run: ./gradlew checkLatestIntelliJPlatformBuild --no-daemon
|
||||
35
platform/jewel/.github/workflows/publish-hotfix.yml
vendored
Normal file
35
platform/jewel/.github/workflows/publish-hotfix.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Publish hotfix artifacts in Space
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag-name:
|
||||
description: 'Hotfix tag to publish'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
publish-current:
|
||||
name: Publish hotfix
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag-name }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run Gradle
|
||||
run: ./gradlew publishMainPublicationToSpaceRepository --no-daemon
|
||||
env:
|
||||
MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
|
||||
MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
|
||||
PGP_PASSWORD: ${{secrets.PGP_PASSWORD}}
|
||||
PGP_PRIVATE_KEY: ${{secrets.PGP_PRIVATE_KEY}}
|
||||
67
platform/jewel/.github/workflows/publish.yml
vendored
Normal file
67
platform/jewel/.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Publish artifacts in Space
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/**'
|
||||
- 'archived-releases/**'
|
||||
|
||||
jobs:
|
||||
publish-current:
|
||||
name: Publish current IJP version (main)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 21
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run Gradle
|
||||
run: ./gradlew publishMainPublicationToSpaceRepository --no-daemon
|
||||
env:
|
||||
MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
|
||||
MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
|
||||
PGP_PASSWORD: ${{secrets.PGP_PASSWORD}}
|
||||
PGP_PRIVATE_KEY: ${{secrets.PGP_PRIVATE_KEY}}
|
||||
|
||||
publish-older:
|
||||
name: Publish older IJP version(s)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
branch-name:
|
||||
- releases/241
|
||||
- releases/242
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.branch-name }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run Gradle
|
||||
run: ./gradlew publishMainPublicationToSpaceRepository --no-daemon
|
||||
env:
|
||||
MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
|
||||
MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
|
||||
PGP_PASSWORD: ${{secrets.PGP_PASSWORD}}
|
||||
PGP_PRIVATE_KEY: ${{secrets.PGP_PRIVATE_KEY}}
|
||||
34
platform/jewel/.github/workflows/stale.yml
vendored
Normal file
34
platform/jewel/.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been inactive for a long time.'
|
||||
stale-pr-message: 'Stale pull request message'
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-issue-labels: 'blocked-externally'
|
||||
exempt-pr-labels: 'blocked-externally'
|
||||
only-labels: 'needs-info'
|
||||
days-before-stale: 7
|
||||
days-before-close: 3
|
||||
labels-to-remove-when-unstale: 'stale'
|
||||
|
||||
109
platform/jewel/.gitignore
vendored
Normal file
109
platform/jewel/.gitignore
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
### macOS template
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
### Gradle template
|
||||
.gradle
|
||||
build/
|
||||
|
||||
### Terraform template
|
||||
# Local .terraform directories
|
||||
**/.terraform/*
|
||||
|
||||
# .tfstate files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/*
|
||||
out/
|
||||
local.properties
|
||||
|
||||
# IDEA/Android Studio project settings ignore exceptions
|
||||
!.idea/codeStyles/
|
||||
!.idea/copyright/
|
||||
!.idea/dataSources.xml
|
||||
!.idea/detekt.xml
|
||||
!.idea/encodings.xml
|
||||
!.idea/externalDependencies.xml
|
||||
!.idea/fileTemplates/
|
||||
!.idea/icon.svg
|
||||
!.idea/icon.png
|
||||
!.idea/icon_dark.png
|
||||
!.idea/inspectionProfiles/
|
||||
!.idea/ktfmt.xml
|
||||
!.idea/ktlint.xml
|
||||
!.idea/ktlint-plugin.xml
|
||||
!.idea/runConfigurations/
|
||||
!.idea/scopes/
|
||||
!.idea/vcs.xml
|
||||
|
||||
### Kotlin template
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
### Windows template
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
### Misc
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Ignore IJP temp folder
|
||||
/.intellijPlatform
|
||||
|
||||
# Ignore release patch generator output
|
||||
/this-release.txt
|
||||
|
||||
# Ignore Kotlin compiler sessions
|
||||
/.kotlin
|
||||
/buildSrc/.kotlin
|
||||
51
platform/jewel/.idea/codeStyles/Project.xml
generated
Normal file
51
platform/jewel/.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="RIGHT_MARGIN" value="120" />
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="true" />
|
||||
<option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="true" />
|
||||
<option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="true" />
|
||||
<option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="true" />
|
||||
<option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="true" />
|
||||
<option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="true" />
|
||||
<option name="CONTINUATION_INDENT_IN_ELVIS" value="true" />
|
||||
<option name="IF_RPAREN_ON_NEW_LINE" value="false" />
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<editorconfig>
|
||||
<option name="ENABLED" value="false" />
|
||||
</editorconfig>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="EXTENDS_LIST_WRAP" value="0" />
|
||||
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
|
||||
<option name="ASSIGNMENT_WRAP" value="5" />
|
||||
<option name="METHOD_ANNOTATION_WRAP" value="5" />
|
||||
<option name="CLASS_ANNOTATION_WRAP" value="5" />
|
||||
<option name="FIELD_ANNOTATION_WRAP" value="5" />
|
||||
<option name="PARAMETER_ANNOTATION_WRAP" value="5" />
|
||||
<option name="VARIABLE_ANNOTATION_WRAP" value="5" />
|
||||
<option name="ENUM_CONSTANTS_WRAP" value="5" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
platform/jewel/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
platform/jewel/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
17
platform/jewel/.idea/detekt.xml
generated
Normal file
17
platform/jewel/.idea/detekt.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DetektPluginSettings">
|
||||
<option name="configurationFilePaths">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/detekt.yml" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="configurationFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/detekt.yml" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="enableDetekt" value="true" />
|
||||
<option name="enableForProjectResult" value="Accepted" />
|
||||
</component>
|
||||
</project>
|
||||
11
platform/jewel/.idea/externalDependencies.xml
generated
Normal file
11
platform/jewel/.idea/externalDependencies.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalDependencies">
|
||||
<plugin id="com.facebook.ktfmt_idea_plugin" min-version="1.2.0.52" />
|
||||
<plugin id="com.nbadal.ktlint" />
|
||||
<plugin id="detekt" />
|
||||
<plugin id="org.jetbrains.compose.desktop.ide" />
|
||||
<plugin id="org.jetbrains.kotlin" />
|
||||
<plugin id="org.norbye.tor.kdocformatter" />
|
||||
</component>
|
||||
</project>
|
||||
7
platform/jewel/.idea/icon.svg
generated
Normal file
7
platform/jewel/.idea/icon.svg
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="black"/>
|
||||
<rect x="256" y="76" width="254.558" height="254.558" transform="rotate(45 256 76)" fill="white"/>
|
||||
<path d="M256 76L436 256H256V76Z" fill="#CCCCCC"/>
|
||||
<path d="M76 256L256 436V256H76Z" fill="#CCCCCC"/>
|
||||
<path d="M256 436L436 256H256V436Z" fill="#808080"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
1085
platform/jewel/.idea/inspectionProfiles/Lint_only.xml
generated
Normal file
1085
platform/jewel/.idea/inspectionProfiles/Lint_only.xml
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
platform/jewel/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
20
platform/jewel/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AntMissingPropertiesFileInspection" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="DeprecatedMavenDependency" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="EditorConfigKeyCorrectness" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="GradlePackageUpdate" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="HardwiredNamespacePrefix" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="IndexZeroUsage" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JavaFxEventHandler" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="NoButtonGroup" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="NoScrollPane" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="StructuralWrap" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||
<inspection_tool class="XsltDeclarations" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="XsltUnusedDeclaration" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="XsltVariableShadowing" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
11
platform/jewel/.idea/ktfmt.xml
generated
Normal file
11
platform/jewel/.idea/ktfmt.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KtfmtSettings">
|
||||
<option name="customBlockIndent" value="4" />
|
||||
<option name="customManageTrailingCommas" value="true" />
|
||||
<option name="customMaxLineLength" value="120" />
|
||||
<option name="enableKtfmt" value="Enabled" />
|
||||
<option name="enabled" value="true" />
|
||||
<option name="uiFormatterStyle" value="Custom" />
|
||||
</component>
|
||||
</project>
|
||||
15
platform/jewel/.idea/ktlint-plugin.xml
generated
Normal file
15
platform/jewel/.idea/ktlint-plugin.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KtLint plugin">
|
||||
<ktlintMode>MANUAL</ktlintMode>
|
||||
<externalJarPaths>
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/../ktlint-compose-0.4.11-all.jar" />
|
||||
</list>
|
||||
</externalJarPaths>
|
||||
</component>
|
||||
<component name="com.nbadal.ktlint.KtlintProjectSettings">
|
||||
<ktlintMode>MANUAL</ktlintMode>
|
||||
<attachToIntellijFormat>false</attachToIntellijFormat>
|
||||
</component>
|
||||
</project>
|
||||
24
platform/jewel/.idea/runConfigurations/Check_IJP_updates.xml
generated
Normal file
24
platform/jewel/.idea/runConfigurations/Check_IJP_updates.xml
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Check IJP updates" type="GradleRunConfiguration" factoryName="Gradle" folderName="Checks">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="checkLatestIntelliJPlatformBuild" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
25
platform/jewel/.idea/runConfigurations/IDE_sample.xml
generated
Normal file
25
platform/jewel/.idea/runConfigurations/IDE_sample.xml
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="IDE sample" type="GradleRunConfiguration" factoryName="Gradle" show_console_on_std_err="true" show_console_on_std_out="true">
|
||||
<log_file alias="IDE Logs" path="$PROJECT_DIR$/samples/ide-plugin/build/idea-sandbox/IC-2024.2/log/idea.log" show_all="true" />
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/ide-plugin" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value=":samples:ide-plugin:runIde" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
25
platform/jewel/.idea/runConfigurations/Pre_push.xml
generated
Normal file
25
platform/jewel/.idea/runConfigurations/Pre_push.xml
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Pre-push" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="apiDump" />
|
||||
<option value="ktfmtFormat" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
24
platform/jewel/.idea/runConfigurations/Reformat_project.xml
generated
Normal file
24
platform/jewel/.idea/runConfigurations/Reformat_project.xml
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Reformat project" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="ktfmtFormat" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
24
platform/jewel/.idea/runConfigurations/Regenerate_icon_keys.xml
generated
Normal file
24
platform/jewel/.idea/runConfigurations/Regenerate_icon_keys.xml
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Regenerate icon keys" type="GradleRunConfiguration" factoryName="Gradle" folderName="Generators">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="--rerun-tasks" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value=":ui:generateAllIconsKeys" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
25
platform/jewel/.idea/runConfigurations/Regenerate_themes.xml
generated
Normal file
25
platform/jewel/.idea/runConfigurations/Regenerate_themes.xml
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Regenerate themes" type="GradleRunConfiguration" factoryName="Gradle" folderName="Generators">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="--rerun-tasks" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value=":int-ui:int-ui-standalone:generateIntUiDarkTheme" />
|
||||
<option value=":int-ui:int-ui-standalone:generateIntUiLightTheme" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
24
platform/jewel/.idea/runConfigurations/Run_checks.xml
generated
Normal file
24
platform/jewel/.idea/runConfigurations/Run_checks.xml
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run checks" type="GradleRunConfiguration" factoryName="Gradle" folderName="Checks">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="check" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
23
platform/jewel/.idea/runConfigurations/Stand_alone_sample.xml
generated
Normal file
23
platform/jewel/.idea/runConfigurations/Stand_alone_sample.xml
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Stand-alone sample" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/standalone" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="run" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
24
platform/jewel/.idea/runConfigurations/Tag_release.xml
generated
Normal file
24
platform/jewel/.idea/runConfigurations/Tag_release.xml
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Tag release" type="GradleRunConfiguration" factoryName="Gradle" folderName="Publish">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value=":tagRelease" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
25
platform/jewel/.idea/runConfigurations/Test_publication.xml
generated
Normal file
25
platform/jewel/.idea/runConfigurations/Test_publication.xml
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Test publication" type="GradleRunConfiguration" factoryName="Gradle" folderName="Publish">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="-PuseCurrentVersion -Pno-sign=true" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="cleanTestPublishArtifacts" />
|
||||
<option value="publishAllPublicationsToLocalTestRepository" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
16
platform/jewel/.idea/vcs.xml
generated
Normal file
16
platform/jewel/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CommitMessageInspectionProfile">
|
||||
<profile version="1.0">
|
||||
<inspection_tool class="BodyLimit" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="SubjectBodySeparation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="SubjectLimit" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
<component name="GitSharedSettings">
|
||||
<option name="synchronizeBranchProtectionRules" value="false" />
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
128
platform/jewel/CODE_OF_CONDUCT.md
Normal file
128
platform/jewel/CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[@jetbrains](https://github.com/jetbrains).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
72
platform/jewel/CONTRIBUTING.md
Normal file
72
platform/jewel/CONTRIBUTING.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Contributions
|
||||
|
||||
We're glad to accept contributions from the community.
|
||||
|
||||
However, **before you open a PR**, please make sure there is a corresponding issue open.
|
||||
In the case of a new feature, a large refactoring, and similar non-trivial changes, it's necessary that a project maintainer greenlights it
|
||||
in advance on the issue page. Non pre-approved PRs that aren't bug fixes or trivial in nature will **not** be accepted.
|
||||
|
||||
# How to contribute
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Familiarity with [pull requests](https://help.github.com/articles/using-pull-requests) and [issues](https://guides.github.com/features/issues/).
|
||||
- Knowledge of [Markdown](https://help.github.com/articles/markdown-basics/) for editing `.md` documents.
|
||||
- Knowledge of [Kotlin](https://kotlinlang.org), [Jetpack Compose](https://d.android.com/jetpack/compose), and
|
||||
[Compose Multiplatform](https://jetbrains.com/lp/compose)
|
||||
|
||||
In particular, this community seeks the following types of contributions:
|
||||
|
||||
- **Ideas**: participate in an issue thread or start your own to have your voice heard.
|
||||
- **Bug fixes**: bugs are inevitable, fixes are always welcome.
|
||||
- **Features**: see the [Contributions](#contributions) section above
|
||||
|
||||
# Conduct
|
||||
|
||||
We are committed to providing a friendly, safe and welcoming environment for
|
||||
all, regardless of gender, sexual orientation, disability, ethnicity, religion,
|
||||
or similar personal characteristic.
|
||||
|
||||
Please avoid using overtly sexual nicknames or other nicknames that
|
||||
might detract from a friendly, safe and welcoming environment for all.
|
||||
|
||||
Please be kind and courteous. There's no need to be mean or rude.
|
||||
Respect that people have differences of opinion and that every design or
|
||||
implementation choice carries a trade-off and numerous costs. There is seldom
|
||||
a right answer, merely an optimal answer given a set of values and
|
||||
circumstances. Try to assume that our community members are nice people
|
||||
and remember, [miscommunication happens](https://hiddenbrain.org/podcast/why-conversations-go-wrong/).
|
||||
|
||||
Please keep unstructured critique to a minimum. If you have solid ideas you
|
||||
want to experiment with, make a fork and see how it works.
|
||||
|
||||
We will exclude you from interaction if you insult, demean or harass anyone.
|
||||
That is not welcome behaviour. We interpret the term "harassment" as
|
||||
including the definition in the [Code of Conduct](CODE_OF_CONDUCT.md);
|
||||
if you have any lack of clarity about what might be included in that concept,
|
||||
please read the definition. In particular, we don't tolerate behavior that
|
||||
excludes people in socially marginalized groups.
|
||||
|
||||
Private harassment is also unacceptable. No matter who you are, if you feel
|
||||
you have been or are being harassed or made uncomfortable by a community
|
||||
member, please contact [one of the maintainers](https://github.com/JetBrains/jewel/graphs/contributors)
|
||||
immediately.
|
||||
Whether you're a regular contributor or a newcomer, we care about
|
||||
making this community a safe place for you and we've got your back.
|
||||
|
||||
Likewise any spamming, trolling, flaming, baiting or other attention-stealing
|
||||
behaviour is not welcome.
|
||||
|
||||
# Communication
|
||||
|
||||
GitHub issues, PRs and discussions are the primary way for communicating about specific proposed
|
||||
changes to this project.
|
||||
|
||||
In all those contexts, please follow the conduct guidelines above. Language issues
|
||||
are often contentious and we'd like to keep discussion brief, civil and focused
|
||||
on what we're actually doing, not wandering off into too much imaginary stuff.
|
||||
|
||||
# Inspiration
|
||||
|
||||
This guide is inspired to [Juno Suárez' contribution guidelines](https://github.com/junosuarez/CONTRIBUTING.md/blob/master/CONTRIBUTING.md),
|
||||
licensed under CC-0 license.
|
||||
201
platform/jewel/LICENSE
Normal file
201
platform/jewel/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022–3 JetBrains s.r.o.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
514
platform/jewel/README.md
Normal file
514
platform/jewel/README.md
Normal file
@@ -0,0 +1,514 @@
|
||||
[](https://github.com/JetBrains#jetbrains-on-github) [](https://github.com/JetBrains/jewel/actions/workflows/build.yml) [](https://github.com/JetBrains/jewel/blob/main/LICENSE) [](https://github.com/JetBrains/jewel/releases/latest) 
|
||||
|
||||
# Jewel: a Compose for Desktop theme
|
||||
|
||||
<img alt="Jewel logo" src="art/jewel-logo.svg" width="20%"/>
|
||||
|
||||
Jewel aims at recreating the IntelliJ Platform's _New UI_ Swing Look and Feel in Compose for Desktop, providing a
|
||||
desktop-optimized theme and set of components.
|
||||
|
||||
> [!CAUTION]
|
||||
> Jewel is moving to the IntelliJ Platform! All active development will move to
|
||||
> https://github.com/JetBrains/intellij-community and this repository will just mirror that.
|
||||
> More information to follow soon — but please **consider the code on this repository as read-only**.
|
||||
|
||||
---
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> This project is in active development, and caution is advised when considering it for production uses. You _can_ use
|
||||
> it, but you should expect APIs to change often, things to move around and/or break, and all that jazz. Binary
|
||||
> compatibility is not guaranteed across releases, and APIs are still in flux and subject to change.
|
||||
>
|
||||
> Writing 3rd party IntelliJ Plugins in Compose for Desktop is currently **not officially supported** by the IntelliJ
|
||||
> Platform. It should work, but your mileage may vary, and if things break you're on your own.
|
||||
>
|
||||
> Use at your own risk!
|
||||
|
||||
Jewel provides an implementation of the IntelliJ Platform themes that can be used in any Compose for Desktop
|
||||
application. Additionally, it has a Swing LaF Bridge that only works in the IntelliJ Platform (i.e., used to create IDE
|
||||
plugins), but automatically mirrors the current Swing LaF into Compose for a native-looking, consistent UI.
|
||||
|
||||
> [!TIP]
|
||||
> <a href="https://www.droidcon.com/2023/11/15/meet-jewelcreate-ide-plugins-in-compose/">
|
||||
> <img src="https://i.vimeocdn.com/video/1749849437-f275e0337faca5cedab742ea157abbafe5a0207d3a59db891a72b6180ce13a6c-d?mh=120" align="left" />
|
||||
> </a>
|
||||
>
|
||||
> If you want to learn more about Jewel and Compose for Desktop and why they're a great, modern solution for your
|
||||
> desktop
|
||||
> UI needs, check out [this talk](https://www.droidcon.com/2023/11/15/meet-jewelcreate-ide-plugins-in-compose/) by Jewel
|
||||
> contributors Sebastiano and Chris.
|
||||
>
|
||||
> It covers why Compose is a viable choice, and an overview of the Jewel project, plus
|
||||
> some real-life use cases.<br clear="left" />
|
||||
|
||||
<br/>
|
||||
|
||||
## Getting started
|
||||
|
||||
The first thing to add is the necessary Gradle plugins, including the Compose Multiplatform plugin. You need to add a
|
||||
custom repository for it in `settings.gradle.kts`:
|
||||
|
||||
```kotlin
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, in your app's `build.gradle.kts`:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
// MUST align with the Kotlin and Compose dependencies in Jewel
|
||||
kotlin("jvm") version "..."
|
||||
id("org.jetbrains.compose") version "..."
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://packages.jetbrains.team/maven/p/kpm/public/")
|
||||
// Any other repositories you need (e.g., mavenCentral())
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> If you use convention plugins to configure your project you might run into issues such as
|
||||
> [this](https://github.com/JetBrains/compose-multiplatform/issues/3748). To solve it, make sure the
|
||||
> plugins are only initialized once — for example, by declaring them in the root `build.gradle.kts`
|
||||
> with `apply false`, and then applying them in all the submodules that need them.
|
||||
|
||||
To use Jewel in your app, you only need to add the relevant dependency. There are two scenarios: standalone Compose for
|
||||
Desktop app, and IntelliJ Platform plugin.
|
||||
|
||||
If you're writing a **standalone app**, then you should depend on the latest `int-ui-standalone-*` artifact:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// See https://github.com/JetBrains/Jewel/releases for the release notes
|
||||
implementation("org.jetbrains.jewel:jewel-int-ui-standalone-[latest platform version]:[jewel version]")
|
||||
|
||||
// Optional, for custom decorated windows:
|
||||
implementation("org.jetbrains.jewel:jewel-int-ui-decorated-window-[latest platform version]:[jewel version]")
|
||||
|
||||
// Do not bring in Material (we use Jewel)
|
||||
implementation(compose.desktop.currentOs) {
|
||||
exclude(group = "org.jetbrains.compose.material")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For an **IntelliJ Platform plugin**, then you should depend on the appropriate `ide-laf-bridge-*` artifact:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// See https://github.com/JetBrains/Jewel/releases for the release notes
|
||||
// The platform version is a supported major IJP version (e.g., 232 or 233 for 2023.2 and 2023.3 respectively)
|
||||
implementation("org.jetbrains.jewel:jewel-ide-laf-bridge-[platform version]:[jewel version]")
|
||||
|
||||
// Do not bring in Material (we use Jewel) and Coroutines (the IDE has its own)
|
||||
api(compose.desktop.currentOs) {
|
||||
exclude(group = "org.jetbrains.compose.material")
|
||||
exclude(group = "org.jetbrains.kotlinx")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
> [!TIP]
|
||||
> It's easier to use version catalogs — you can use the Jewel [version catalog](gradle/libs.versions.toml) as reference.
|
||||
|
||||
<br/>
|
||||
|
||||
## Using ProGuard/obfuscation/minification
|
||||
|
||||
Jewel doesn't officially support using ProGuard to minimize and/or obfuscate your code, and there is currently no plan
|
||||
to.
|
||||
That said, people are reporting successes in using it. Please note that there is no guarantee that it will keep working,
|
||||
and you most definitely need to have some rules in place. We don't provide any official rule set, but these have been
|
||||
known
|
||||
to work for some: https://github.com/romainguy/kotlin-explorer/blob/main/compose-desktop.pro
|
||||
|
||||
> [!IMPORTANT]
|
||||
> We won't accept bug reports for issues caused by the use of ProGuard or similar tools.
|
||||
|
||||
## Dependencies matrix
|
||||
|
||||
Jewel is in continuous development and we focus on supporting only the Compose version we use internally.
|
||||
You can see the latest supported version
|
||||
in [libs.versions.toml](https://github.com/JetBrains/jewel/blob/main/gradle/libs.versions.toml).
|
||||
|
||||
Different versions of Compose are not guaranteed to work with different versions of Jewel.
|
||||
|
||||
The Compose Compiler version used is the latest compatible with the given Kotlin version. See
|
||||
[here](https://developer.android.com/jetpack/androidx/releases/compose-compiler) for the Compose
|
||||
Compiler release notes, which indicate the compatibility.
|
||||
|
||||
The minimum supported Kotlin version is dictated by the minimum supported IntelliJ IDEA platform.
|
||||
|
||||
## Project structure
|
||||
|
||||
The project is split in modules:
|
||||
|
||||
1. `buildSrc` contains the build logic, including:
|
||||
* The `jewel` and `jewel-publish` configuration plugins
|
||||
* The `jewel-check-public-api` and `jewel-linting` configuration plugins
|
||||
* The Theme Palette generator plugin
|
||||
* The Studio Releases generator plugin
|
||||
2. `foundation` contains the foundational Jewel functionality:
|
||||
* Basic components without strong styling (e.g., `SelectableLazyColumn`, `BasicLazyTree`)
|
||||
* The `JewelTheme` interface with a few basic composition locals
|
||||
* The state management primitives
|
||||
* The Jewel annotations
|
||||
* A few other primitives
|
||||
3. `ui` contains all the styled components and custom painters logic
|
||||
4. `decorated-window` contains basic, unstyled functionality to have custom window decoration on the JetBrains Runtime
|
||||
5. `int-ui` contains two modules:
|
||||
* `int-ui-standalone` has a standalone version of the Int UI styling values that can be used in any Compose for
|
||||
Desktop app
|
||||
* `int-ui-decorated-window` has a standalone version of the Int UI styling values for the custom window decoration
|
||||
that can be used in any Compose for Desktop app
|
||||
6. `ide-laf-bridge` contains the Swing LaF bridge to use in IntelliJ Platform plugins (see more below)
|
||||
7. `markdown` contains a few modules:
|
||||
* `core` the core logic for parsing and rendering Markdown documents with Jewel, using GitHub-like styling
|
||||
* `extension` contains several extensions to the base CommonMark specs that can be used to add more features
|
||||
* `ide-laf-bridge-styling` contains the IntelliJ Platform bridge theming for the Markdown renderer
|
||||
* `int-ui-standalone-styling` contains the standalone Int UI theming for the Markdown renderer
|
||||
8. `samples` contains the example apps, which showcase the available components:
|
||||
* `standalone` is a regular CfD app, using the standalone theme definitions and custom window decoration
|
||||
* `ide-plugin` is an IntelliJ plugin that showcases the use of the Swing Bridge
|
||||
|
||||
## Branching strategy and IJ Platforms
|
||||
|
||||
Code on the main branch is developed and tested against the current latest IntelliJ Platform version.
|
||||
|
||||
When the EAP for a new major version starts, we cut a `releases/xxx` release branch, where `xxx` is the tracked major
|
||||
IJP version. At that point, the main branch starts tracking the latest available major IJP version, and changes are
|
||||
cherry-picked into each release branch as needed. All active release branches have the same functionality (where
|
||||
supported by the corresponding IJP version), but might differ in platform version-specific fixes and internals.
|
||||
|
||||
The standalone Int UI theme will always work the same way as the latest major IJP version; release branches will not
|
||||
include the `int-ui` module, which is always released from the main branch.
|
||||
|
||||
Releases of Jewel are always cut from a tag on the main branch; the HEAD of each `releases/xxx` branch is then tagged
|
||||
as `[mainTag]-xxx`, and used to publish the artifacts for that major IJP version.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> We only support the latest build of IJP for each major IJP version. If the latest 233 version is 2023.3.3, for
|
||||
> example, we will only guarantee that Jewel works on that. Versions 2023.3.0–2023.3.2 might or might not work.
|
||||
|
||||
> [!CAUTION]
|
||||
> When you target Android Studio, you might encounter issues due to Studio shipping its own (older) version of Jewel
|
||||
> and Compose for Desktop. If you want to target Android Studio, you'll need to shadow the CfD and Jewel dependencies
|
||||
> until that dependency isn't leaked on the classpath by Studio anymore. You can look at how the
|
||||
> [Package Search](https://github.com/JetBrains/package-search-intellij-plugin) plugin implements shadowing.
|
||||
|
||||
## Int UI Standalone theme
|
||||
|
||||
The standalone theme can be used in any Compose for Desktop app. You use it as a normal theme, and you can customise it
|
||||
to your heart's content. By default, it matches the official Int UI specs.
|
||||
|
||||
For an example on how to set up a standalone app, you can refer to
|
||||
the [`standalone` sample](samples/standalone/build.gradle.kts).
|
||||
|
||||
> [!WARNING]
|
||||
> Note that Jewel **requires** the JetBrains Runtime to work correctly. Some features like font loading depend on it,
|
||||
> as it has extra features and patches for UI functionalities that aren't available in other JDKs.
|
||||
> We **do not support** running Jewel on any other JDK.
|
||||
|
||||
To use Jewel components in a non-IntelliJ Platform environment, you need to wrap your UI hierarchy in a `IntUiTheme`
|
||||
composable:
|
||||
|
||||
```kotlin
|
||||
IntUiTheme(isDark = false) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
If you want more control over the theming, you can use other `IntUiTheme` overloads, like the standalone sample does.
|
||||
|
||||
### Custom window decoration
|
||||
|
||||
The JetBrains Runtime allows windows to have a custom decoration instead of the regular title bar.
|
||||
|
||||

|
||||
|
||||
The standalone sample app shows how to easily get something that looks like a JetBrains IDE; if you want to go _very_
|
||||
custom, you only need to depend on the `decorated-window` module, which contains all the required primitives, but not
|
||||
the Int UI styling.
|
||||
|
||||
To get an IntelliJ-like custom title bar, you need to pass the window decoration styling to your theme call, and add the
|
||||
`DecoratedWindow` composable at the top level of the theme:
|
||||
|
||||
```kotlin
|
||||
IntUiTheme(
|
||||
theme = themeDefinition,
|
||||
styling = ComponentStyling.default().decoratedWindow(
|
||||
titleBarStyle = TitleBarStyle.light()
|
||||
),
|
||||
) {
|
||||
DecoratedWindow(
|
||||
onCloseRequest = { exitApplication() },
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Running on the IntelliJ Platform: the Swing bridge
|
||||
|
||||
Jewel includes a crucial element for proper integration with the IDE: a bridge between the Swing components — theme
|
||||
and LaF — and the Compose world.
|
||||
|
||||
This bridge ensures that we pick up the colours, typography, metrics, and images as defined in the current IntelliJ
|
||||
theme, and apply them to the Compose components as well. This means Jewel will automatically adapt to IntelliJ Platform
|
||||
themes that use the [standard theming](https://plugins.jetbrains.com/docs/intellij/themes-getting-started.html)
|
||||
mechanisms.
|
||||
|
||||
> [!NOTE]
|
||||
> IntelliJ themes that use non-standard mechanisms (such as providing custom UI implementations for Swing components)
|
||||
> are not, and can never, be supported.
|
||||
|
||||
If you're writing an IntelliJ Platform plugin, you should use the `SwingBridgeTheme` instead of the standalone theme:
|
||||
|
||||
```kotlin
|
||||
SwingBridgeTheme {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Supported IntelliJ Platform versions
|
||||
|
||||
To use Jewel in the IntelliJ Platform, you should depend on the appropriate `jewel-ide-laf-bridge-*` artifact, which
|
||||
will bring in the necessary transitive dependencies. These are the currently supported versions of the IntelliJ Platform
|
||||
and the branch on which the corresponding bridge code lives:
|
||||
|
||||
| IntelliJ Platform version(s) | Branch to use |
|
||||
|------------------------------|-------------------------|
|
||||
| 2024.3 (EAP 6+) | `main` |
|
||||
| 2024.2 (beta 1+) | `releases/242` |
|
||||
| 2024.1 (EAP 3+) | `releases/241` |
|
||||
| 2023.3 (**archived**) | `archived-releases/233` |
|
||||
| 2023.2 (**archived**) | `archived-releases/232` |
|
||||
| 2023.1 or older | **Not supported** |
|
||||
|
||||
For an example on how to set up an IntelliJ Plugin, you can refer to
|
||||
the [`ide-plugin` sample](samples/ide-plugin/build.gradle.kts).
|
||||
|
||||
## Icons
|
||||
|
||||
Loading icons is best done with the `Icon` composable, which offers a key-based API that is portable across bridge and
|
||||
standalone modules. Icon keys implement the `IconKey` interface, which is then internally used to obtain a resource path
|
||||
to load the icon from.
|
||||
|
||||
```kotlin
|
||||
Icon(key = MyIconKeys.myIcon, contentDescription = "My icon")
|
||||
```
|
||||
|
||||
### Loading icons from the IntelliJ Platform
|
||||
|
||||
If you want to load an IJ platform icon, you can use `AllIconsKeys`, which is generated from the `AllIcons` platform
|
||||
file. When using this in an IJ plugin, make sure you are using a version of the Jewel library matching the platform
|
||||
version, because icons are known to shift between major versions — and sometimes, minor versions, too.
|
||||
|
||||
To use icons from `AllIconsKeys` in an IJ plugin, you don't need to do anything, as the icons are in the classpath by
|
||||
default. If you want to use icons in a standalone app, you'll need to make sure the icons you want are on the classpath.
|
||||
You can either copy the necessary icons in your resources, matching exactly the path they have in the IDE, or you can
|
||||
add a dependency to the `com.jetbrains.intellij.platform:icons` artifact, which contains all the icons that end up in
|
||||
`AllIconsKeys`. The latter is the recommended approach, since it's easy and the icons don't take up much disk space.
|
||||
|
||||
Add this to your **standalone app** build script:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("com.jetbrains.intellij.platform:icons:[ijpVersion]")
|
||||
// ...
|
||||
}
|
||||
|
||||
repositories {
|
||||
// Choose either of these two, depending on whether you're using a stable IJP or not
|
||||
maven("https://www.jetbrains.com/intellij-repository/releases")
|
||||
maven("https://www.jetbrains.com/intellij-repository/snapshots")
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you are targeting an IntelliJ plugin, you don't need this additional setup since the icons are provided by the
|
||||
> platform itself.
|
||||
|
||||
### Loading your own icons
|
||||
|
||||
To access your own icons, you'll need to create and maintain the `IconKey`s for them. We found that the easiest way when
|
||||
you have up to a few dozen icons is to manually create an icon keys holder, like the ones we have in our samples. If you
|
||||
have many more, you should consider generating these holders, instead.
|
||||
|
||||
In your holders, you can choose which implementation of `IconKey` to use:
|
||||
* If your icons do not need to change between old UI and new UI, you can use the simpler `PathIconKey`
|
||||
* If your icons are different in old and new UI, you should use `IntelliJIconKey`, which accepts two paths, one per
|
||||
variant
|
||||
* If you have different needs, you can also implement your own version of `IconKey`
|
||||
|
||||
### Painter hints
|
||||
|
||||
Jewel has an API to influence the loading and drawing of icons, called `PainterHint`. `Icon` composables have overloads
|
||||
that take zero, one or more `PainterHint`s that will be used to compute the end result that shows up on screen.
|
||||
|
||||
`PainterHint`s can change the icon path (by adding a prefix/suffix, or changing it completely), tweak the contents of an
|
||||
image (SVG patching, XML patching, bitmap patching), add decorations (e.g., badges), or do nothing at all (`None`). We
|
||||
have several types of built-in `PainterHint`s which should cover all needs; if you find some use case that is not yet
|
||||
handled, please file a feature request and we'll evaluate it.
|
||||
|
||||
Both standalone and bridge themes provide a default set of implicit `PainterHint`s, for example to implement runtime
|
||||
patching, like the IDE does. You can also use `PainterHint`s to affect how an icon will be drawn, or to select a
|
||||
specific icon file, based on some criteria (e.g., `Size`).
|
||||
|
||||
If you have a _stateful_ icon, that is if you need to display different icons based on some state, you can use the
|
||||
`Icon(..., hint)` and `Icon(..., hints)` overloads. You can then use one of the state-mapping `PainterHint` to let
|
||||
Jewel load the appropriate icon automatically:
|
||||
|
||||
```kotlin
|
||||
// myState implements SelectableComponentState and has a ToggleableState property
|
||||
val indeterminateHint =
|
||||
if (myState.toggleableState == ToggleableState.Indeterminate) {
|
||||
IndeterminateHint
|
||||
} else {
|
||||
PainterHint.None
|
||||
}
|
||||
|
||||
Icon(
|
||||
key = myKey,
|
||||
contentDescription = "My icon",
|
||||
indeterminateHint,
|
||||
Selected(myState),
|
||||
Stateful(myState),
|
||||
)
|
||||
```
|
||||
|
||||
Where the `IndeterminateHint` looks like this:
|
||||
|
||||
```kotlin
|
||||
private object IndeterminateHint : PainterSuffixHint() {
|
||||
override fun suffix(): String = "Indeterminate"
|
||||
}
|
||||
```
|
||||
|
||||
Assuming the PainterProvider has a base path of `components/myIcon.svg`, Jewel will automatically translate it to the
|
||||
right path based on the state. If you want to learn more about this system, look at the `PainterHint` interface and its
|
||||
implementations.
|
||||
|
||||
Please look at the `PainterHint` implementations and our samples for further information.
|
||||
|
||||
### Default icon runtime patching
|
||||
|
||||
Jewel emulates the under-the-hood machinations that happen in the IntelliJ Platform when loading icons. Specifically,
|
||||
the resource will be subject to some transformations before being loaded. This is built on the `PainterHint` API we
|
||||
described above.
|
||||
|
||||
For example, in the IDE, if New UI is active, the icon path may be replaced with a different one. Some key colors in SVG
|
||||
icons will also be replaced based on the current theme. See
|
||||
[the docs](https://plugins.jetbrains.com/docs/intellij/work-with-icons-and-images.html#new-ui-icons).
|
||||
|
||||
Beyond that, even in standalone, Jewel will pick up icons with the appropriate dark/light variant for the current theme,
|
||||
and for bitmap icons it will try to pick the 2x variants based on the `LocalDensity`.
|
||||
|
||||
## Fonts
|
||||
|
||||
To load a system font, you can obtain it by its family name:
|
||||
|
||||
```kotlin
|
||||
val myFamily = FontFamily("My Family")
|
||||
```
|
||||
|
||||
If you want to use a font embedded in the JetBrains Runtime, you can use the `EmbeddedFontFamily` API instead:
|
||||
|
||||
```kotlin
|
||||
import javax.swing.text.StyledEditorKit.FontFamilyAction
|
||||
|
||||
// Will return null if no matching font family exists in the JBR
|
||||
val myEmbeddedFamily = EmbeddedFontFamily("Embedded family")
|
||||
|
||||
// It's recommended to load a fallback family when dealing with embedded familes
|
||||
val myFamily = myEmbeddedFamily ?: FontFamily("Fallback family")
|
||||
```
|
||||
|
||||
You can obtain a `FontFamily` from any `java.awt.Font` — including from `JBFont`s — by using the `asComposeFontFamily()`
|
||||
API:
|
||||
|
||||
```kotlin
|
||||
val myAwtFamily = myFont.asComposeFontFamily()
|
||||
|
||||
// This will attempt to resolve the logical AWT font
|
||||
val myLogicalFamily = Font("Dialog").asComposeFontFamily()
|
||||
|
||||
// This only works in the IntelliJ Platform,
|
||||
// since JBFont is only available there
|
||||
val myLabelFamily = JBFont.label().asComposeFontFamily()
|
||||
```
|
||||
|
||||
## Swing interoperability
|
||||
|
||||
As this is Compose for Desktop, you get a good degree of interoperability with Swing. To avoid glitches and z-order
|
||||
issues, you should enable the
|
||||
[experimental Swing rendering pipeline](https://blog.jetbrains.com/kotlin/2023/08/compose-multiplatform-1-5-0-release/#enhanced-swing-interop)
|
||||
before you initialize Compose content.
|
||||
|
||||
The `ToolWindow.addComposeTab()` extension function provided by the `ide-laf-bridge` module will take care of that for
|
||||
you. However, if you want to also enable it in other scenarios and in standalone applications, you can call the
|
||||
`enableNewSwingCompositing()` function in your Compose entry points (that is, right before creating a `ComposePanel`).
|
||||
|
||||
> [!NOTE]
|
||||
> The new Swing rendering pipeline is experimental and may have performance repercussions when using infinitely
|
||||
> repeating animations. This is a known issue by the Compose Multiplatform team, that requires changes in the Java
|
||||
> runtime to fix. Once the required changes are made in the JetBrains Runtime, we'll remove this notice.
|
||||
|
||||
## Written with Jewel
|
||||
|
||||
Here is a small selection of projects that use Compose for Desktop and Jewel:
|
||||
|
||||
* [Package Search](https://github.com/JetBrains/package-search-intellij-plugin) (IntelliJ Platform plugin)
|
||||
* [Kotlin Explorer](https://github.com/romainguy/kotlin-explorer) (standalone app)
|
||||
* New task-based Profiler UI in Android Studio Koala
|
||||
* ...and more to come!
|
||||
|
||||
## Throubleshooting
|
||||
|
||||
### Git push hook is not working?
|
||||
|
||||
On git push you see:
|
||||
|
||||
```bash
|
||||
error: cannot spawn .git/hooks/pre-push: No such file or directory
|
||||
error: waitpid for (NULL) failed: No child processes
|
||||
```
|
||||
|
||||
Try running `git lfs update --force`.
|
||||
|
||||
## Need help?
|
||||
|
||||
You can find help on the [`#jewel`](https://app.slack.com/client/T09229ZC6/C05T8U2C31T) channel on the Kotlin Slack.
|
||||
If you don't already have access to the Kotlin Slack, you can request it
|
||||
[here](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up).
|
||||
|
||||
## License
|
||||
|
||||
Jewel is licensed under the [Apache 2.0 license](https://github.com/JetBrains/jewel/blob/main/LICENSE).
|
||||
|
||||
```
|
||||
Copyright 2022–4 JetBrains s.r.o.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
40
platform/jewel/RELEASING.md
Normal file
40
platform/jewel/RELEASING.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Releasing new versions of Jewel
|
||||
|
||||
The release process is mostly automated, but it requires manual prep steps.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Don't arbitrarily create releases. Always obtain written go-ahead from @rock3r before cutting a release!
|
||||
|
||||
1. Update the Jewel version in [gradle.properties]
|
||||
2. Commit the change with a message like "Bump version to x.y.z"
|
||||
3. Ensure that Jewel works correctly in all its parts:
|
||||
* Run all tests and checks
|
||||
* Run the standalone sample and check _everything_ works as expected (in all themes, with/without Swing compat, ...)
|
||||
* Run the IDE sample and check _everything_ works as expected (in all themes)
|
||||
4. _For every branch in `releases/`_:
|
||||
a. Check out the branch
|
||||
b. Merge the `main` branch in using the _Merge_ dialog and these options:
|
||||

|
||||
c. If needed, solve merge conflicts, as it makes sense
|
||||
> [!CAUTION]
|
||||
> Merging is the most delicate part. Not all changes need backporting, and not all changes can be
|
||||
> backported without adapting to how an older IJP version works. **Triple check all backports!**
|
||||
|
||||
d. Repeat step 3 for this branch
|
||||
5. Once all branches are ready, run the `:tagRelease` task, which will validate that the merge was done properly, and
|
||||
that the version has been bumped
|
||||
6. Push all the branches and all the new tags
|
||||
7. Open the [tags page](https://github.com/JetBrains/jewel/tags), select the _base_ tag (e.g., `v1.0.0`, and not any of
|
||||
the `v1.0.0-*` sub-tags)
|
||||
8. Create a release from the tag, using the auto-generated release notes. Add any further information that may be useful
|
||||
to users, including any breaking API or behaviour changes
|
||||
* You can use an LLM to help you with this step if you want. Use the [generate-release-patch.sh] script to generate
|
||||
a
|
||||
patch file with all the changes since the last tag (make sure to run it on main!). You can then use a service like
|
||||
Gemini Advanced or ChatGPT to generate release notes from it.
|
||||
* An example prompt that produces decent results with Gemini
|
||||
Advanced: https://gist.github.com/rock3r/3ed8ec836143049da834505ce5315fce
|
||||
* **ALWAYS double check** the LLM output. Don't trust it! Treat it only a starting point to help you, not the end
|
||||
result.
|
||||
9. Wait for the [publishing CI job](https://github.com/JetBrains/jewel/actions/workflows/publish.yml) to finish running
|
||||
10. Done!
|
||||
14
platform/jewel/SECURITY.md
Normal file
14
platform/jewel/SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
This code is provided as-is. We'll fix issues as quickly as possible, but no
|
||||
warranty of any kind is provided that the project is problem-free. Please do
|
||||
not use this software in security-critical situations without a proper
|
||||
analysis and vetting of its sources first from a security standpoint — we
|
||||
have not done it.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you find a vulnerability, please report it as an issue. We'll address it
|
||||
as soon as we can.
|
||||
BIN
platform/jewel/art/docs/custom-chrome.png
Normal file
BIN
platform/jewel/art/docs/custom-chrome.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
platform/jewel/art/docs/merge-dialog.png
Normal file
BIN
platform/jewel/art/docs/merge-dialog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
7
platform/jewel/art/jewel-logo.svg
Normal file
7
platform/jewel/art/jewel-logo.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="black"/>
|
||||
<rect x="256" y="76" width="254.558" height="254.558" transform="rotate(45 256 76)" fill="white"/>
|
||||
<path d="M256 76L436 256H256V76Z" fill="#CCCCCC"/>
|
||||
<path d="M76 256L256 436V256H76Z" fill="#CCCCCC"/>
|
||||
<path d="M256 436L436 256H256V436Z" fill="#808080"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
185
platform/jewel/build.gradle.kts
Normal file
185
platform/jewel/build.gradle.kts
Normal file
@@ -0,0 +1,185 @@
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.composeDesktop) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
`jewel-linting`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
sarif(projects.decoratedWindow)
|
||||
sarif(projects.foundation)
|
||||
sarif(projects.ideLafBridge)
|
||||
sarif(projects.intUi.intUiDecoratedWindow)
|
||||
sarif(projects.intUi.intUiStandalone)
|
||||
sarif(projects.markdown.core)
|
||||
sarif(projects.markdown.extension.autolink)
|
||||
sarif(projects.markdown.extension.gfmAlerts)
|
||||
sarif(projects.markdown.ideLafBridgeStyling)
|
||||
sarif(projects.markdown.intUiStandaloneStyling)
|
||||
sarif(projects.samples.idePlugin)
|
||||
sarif(projects.samples.standalone)
|
||||
sarif(projects.ui)
|
||||
}
|
||||
|
||||
// TODO remove this once the Skiko fix makes it into CMP 1.7.1
|
||||
allprojects {
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency {
|
||||
if (requested.group == "org.jetbrains.skiko") {
|
||||
useVersion("0.8.17")
|
||||
because("Contains important memory usage fix")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
// val mergeSarifReports by
|
||||
// registering(MergeSarifTask::class) {
|
||||
// source(configurations.outgoingSarif)
|
||||
// include { it.file.extension == "sarif" }
|
||||
// }
|
||||
//
|
||||
// register("check") { dependsOn(mergeSarifReports) }
|
||||
|
||||
register("tagRelease") {
|
||||
description = "Tags main branch and releases branches with provided tag name"
|
||||
group = "release"
|
||||
|
||||
doFirst {
|
||||
val rawReleaseVersion =
|
||||
((project.property("jewel.release.version") as String?)?.takeIf { it.isNotBlank() }
|
||||
?: throw GradleException("Please provide a jewel.release.version in gradle.properties"))
|
||||
|
||||
val releaseName = "v$rawReleaseVersion"
|
||||
|
||||
val stdOut = ByteArrayOutputStream()
|
||||
|
||||
// Check we're on the main branch
|
||||
logger.info("Checking current branch is main...")
|
||||
exec {
|
||||
commandLine = listOf("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
|
||||
val currentBranch = stdOut.use { it.toString() }.trim()
|
||||
if (currentBranch != "main") {
|
||||
throw GradleException("This task must only be run on the main branch")
|
||||
}
|
||||
|
||||
// Check tag doesn't already exist
|
||||
logger.info("Checking current branch is main...")
|
||||
stdOut.reset()
|
||||
exec {
|
||||
commandLine = listOf("git", "tag")
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
|
||||
if (stdOut.toString().trim().lines().any { it == releaseName }) {
|
||||
throw GradleException("The tag $releaseName already exists!")
|
||||
}
|
||||
|
||||
// Check there are no uncommitted changes
|
||||
logger.info("Checking all changes have been committed...")
|
||||
stdOut.reset()
|
||||
exec {
|
||||
commandLine = listOf("git", "status", "--porcelain")
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
|
||||
if (stdOut.toString().isNotBlank()) {
|
||||
throw GradleException("Please commit all changes before tagging a release")
|
||||
}
|
||||
|
||||
// Get the current HEAD hash
|
||||
logger.info("Getting HEAD hash...")
|
||||
stdOut.reset()
|
||||
exec {
|
||||
commandLine = listOf("git", "rev-parse", "HEAD")
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
|
||||
val currentHead = stdOut.use { it.toString() }.trim()
|
||||
|
||||
// Enumerate the release branches
|
||||
logger.info("Enumerating release branches...")
|
||||
stdOut.reset()
|
||||
exec {
|
||||
commandLine = listOf("git", "branch")
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
|
||||
val releaseBranches =
|
||||
stdOut
|
||||
.use { it.toString() }
|
||||
.lines()
|
||||
.filter { it.trim().startsWith("releases/") }
|
||||
.map { it.trim().removePrefix("releases/") }
|
||||
|
||||
if (releaseBranches.isEmpty()) {
|
||||
throw GradleException("No local release branches found, make sure they exist locally")
|
||||
}
|
||||
|
||||
logger.lifecycle("Release branches: ${releaseBranches.joinToString { "releases/$it" }}")
|
||||
|
||||
// Check all release branches have gotten the latest from main
|
||||
logger.info("Validating release branches...")
|
||||
for (branch in releaseBranches) {
|
||||
stdOut.reset()
|
||||
exec {
|
||||
commandLine = listOf("git", "merge-base", "main", "releases/$branch")
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
|
||||
val mergeBase = stdOut.use { it.toString() }.trim()
|
||||
if (mergeBase != currentHead) {
|
||||
throw GradleException("Branch releases/$branch is not up-to-date with main!")
|
||||
}
|
||||
}
|
||||
|
||||
// Tag main branch
|
||||
logger.lifecycle("Tagging head of main branch as $releaseName...")
|
||||
exec { commandLine = listOf("git", "tag", releaseName) }.assertNormalExitValue()
|
||||
|
||||
// Tag release branches
|
||||
for (branch in releaseBranches) {
|
||||
val branchTagName = "$releaseName-$branch"
|
||||
logger.lifecycle("Tagging head of branch releases/$branch as $branchTagName...")
|
||||
stdOut.reset()
|
||||
|
||||
logger.info("Getting branch head commit...")
|
||||
exec {
|
||||
commandLine = listOf("git", "rev-parse", "releases/$branch")
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
|
||||
val branchHead = stdOut.use { it.toString() }.trim()
|
||||
logger.info("HEAD of releases/$branch is $branchHead")
|
||||
|
||||
logger.info("Tagging commit ${branchHead.take(7)} as $branchTagName")
|
||||
stdOut.reset()
|
||||
exec {
|
||||
commandLine = listOf("git", "tag", branchTagName, branchHead)
|
||||
standardOutput = stdOut
|
||||
}
|
||||
.assertNormalExitValue()
|
||||
}
|
||||
|
||||
logger.info("All done!")
|
||||
}
|
||||
}
|
||||
|
||||
register<Delete>("cleanTestPublishArtifacts") { delete(rootProject.layout.buildDirectory.dir("maven-test")) }
|
||||
|
||||
register<Delete>("clean") { delete(rootProject.layout.buildDirectory) }
|
||||
}
|
||||
36
platform/jewel/buildSrc/build.gradle.kts
Normal file
36
platform/jewel/buildSrc/build.gradle.kts
Normal file
@@ -0,0 +1,36 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
val properties = Properties()
|
||||
|
||||
project.file("../gradle.properties").inputStream().use { properties.load(it) }
|
||||
|
||||
val jdkLevel = properties.getProperty("jdk.level") as String
|
||||
|
||||
kotlin {
|
||||
jvmToolchain { languageVersion = JavaLanguageVersion.of(jdkLevel) }
|
||||
|
||||
sourceSets { all { languageSettings { optIn("kotlinx.serialization.ExperimentalSerializationApi") } } }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.detekt.gradlePlugin)
|
||||
implementation(libs.dokka.gradlePlugin)
|
||||
implementation(libs.kotlin.gradlePlugin)
|
||||
implementation(libs.kotlinSarif)
|
||||
implementation(libs.kotlinpoet)
|
||||
implementation(libs.kotlinter.gradlePlugin)
|
||||
implementation(libs.ktfmt.gradlePlugin)
|
||||
implementation(libs.kotlinx.binaryCompatValidator.gradlePlugin)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.poko.gradlePlugin)
|
||||
|
||||
// Enables using type-safe accessors to reference plugins from the [plugins] block defined in
|
||||
// version catalogs.
|
||||
// Context: https://github.com/gradle/gradle/issues/15383#issuecomment-779893192
|
||||
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
|
||||
}
|
||||
19
platform/jewel/buildSrc/settings.gradle.kts
Normal file
19
platform/jewel/buildSrc/settings.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
rootProject.name = "buildSrc"
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
maven("https://www.jetbrains.com/intellij-repository/releases")
|
||||
maven("https://www.jetbrains.com/intellij-repository/snapshots")
|
||||
maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
|
||||
}
|
||||
63
platform/jewel/buildSrc/src/main/kotlin/MergeSarifTask.kt
Normal file
63
platform/jewel/buildSrc/src/main/kotlin/MergeSarifTask.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
import io.github.detekt.sarif4k.Run
|
||||
import io.github.detekt.sarif4k.SarifSchema210
|
||||
import io.github.detekt.sarif4k.Tool
|
||||
import io.github.detekt.sarif4k.ToolComponent
|
||||
import io.github.detekt.sarif4k.Version
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.CacheableTask
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.SourceTask
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
private const val SARIF_SCHEMA =
|
||||
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
|
||||
|
||||
@CacheableTask
|
||||
open class MergeSarifTask : SourceTask() {
|
||||
|
||||
init {
|
||||
group = "verification"
|
||||
}
|
||||
|
||||
@get:OutputFile
|
||||
val mergedSarifPath: RegularFileProperty =
|
||||
project.objects.fileProperty().convention(project.layout.buildDirectory.file("reports/static-analysis.sarif"))
|
||||
|
||||
@TaskAction
|
||||
fun merge() {
|
||||
val json = Json { prettyPrint = true }
|
||||
|
||||
logger.lifecycle("Merging ${source.files.size} SARIF file(s)...")
|
||||
logger.lifecycle(source.files.joinToString("\n") { " * ~${it.path.removePrefix(project.rootDir.path)}" })
|
||||
|
||||
val merged =
|
||||
SarifSchema210(
|
||||
schema = SARIF_SCHEMA,
|
||||
version = Version.The210,
|
||||
runs =
|
||||
source.files
|
||||
.asSequence()
|
||||
.filter { it.extension == "sarif" }
|
||||
.map { file -> file.inputStream().use { json.decodeFromStream<SarifSchema210>(it) } }
|
||||
.flatMap { report -> report.runs }
|
||||
.groupBy { run -> run.tool.driver.guid ?: run.tool.driver.name }
|
||||
.values
|
||||
.asSequence()
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { runs ->
|
||||
Run(
|
||||
results = runs.flatMap { it.results.orEmpty() },
|
||||
tool = Tool(driver = ToolComponent(name = "Jewel static analysis")),
|
||||
)
|
||||
}
|
||||
.toList(),
|
||||
)
|
||||
|
||||
logger.lifecycle("Merged SARIF file contains ${merged.runs.size} run(s)")
|
||||
logger.info("Writing merged SARIF file to $mergedSarifPath...")
|
||||
mergedSarifPath.asFile.get().outputStream().use { json.encodeToStream(merged, it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
@file:Suppress("UnstableApiUsage", "UnusedImports")
|
||||
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.publish.PublishingExtension
|
||||
import org.gradle.api.publish.maven.MavenPom
|
||||
import org.gradle.kotlin.dsl.assign
|
||||
import org.gradle.kotlin.dsl.maven
|
||||
|
||||
internal fun PublishingExtension.configureJewelRepositories(project: Project) {
|
||||
repositories {
|
||||
maven("https://packages.jetbrains.team/maven/p/kpm/public") {
|
||||
name = "Space"
|
||||
credentials {
|
||||
username = System.getenv("MAVEN_SPACE_USERNAME")
|
||||
password = System.getenv("MAVEN_SPACE_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
maven(project.rootProject.layout.buildDirectory.dir("maven-test")) { name = "LocalTest" }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MavenPom.configureJewelPom() {
|
||||
name = "Jewel"
|
||||
description = "A theme for Compose for Desktop that implements the IntelliJ Platform look and feel."
|
||||
url = "https://github.com/JetBrains/jewel"
|
||||
licenses {
|
||||
license {
|
||||
name = "Apache License 2.0"
|
||||
url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = "scm:git:https://github.com/JetBrains/jewel.git"
|
||||
developerConnection = "scm:git:https://github.com/JetBrains/jewel.git"
|
||||
url = "https://github.com/JetBrains/jewel"
|
||||
}
|
||||
}
|
||||
128
platform/jewel/buildSrc/src/main/kotlin/ValidatePublicApiTask.kt
Normal file
128
platform/jewel/buildSrc/src/main/kotlin/ValidatePublicApiTask.kt
Normal file
@@ -0,0 +1,128 @@
|
||||
import java.io.File
|
||||
import java.util.Stack
|
||||
import java.util.regex.PatternSyntaxException
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.tasks.CacheableTask
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.SourceTask
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
@CacheableTask
|
||||
open class ValidatePublicApiTask : SourceTask() {
|
||||
|
||||
@Input var excludedClassRegexes: Set<String> = emptySet()
|
||||
|
||||
init {
|
||||
group = "verification"
|
||||
|
||||
// The output is never really used, it is here for cacheability reasons only
|
||||
outputs.file(project.layout.buildDirectory.file("apiValidationRun"))
|
||||
}
|
||||
|
||||
private val classFqnRegex = "public (?:\\w+ )*class (\\S+)\\b".toRegex()
|
||||
|
||||
@Suppress("ConvertToStringTemplate") // The odd concatenation is needed because of $; escapes get confused
|
||||
private val copyMethodRegex = ("public static synthetic fun copy(-\\w+)?" + "\\$" + "default\\b").toRegex()
|
||||
|
||||
@TaskAction
|
||||
fun validatePublicApi() {
|
||||
logger.info("Validating ${source.files.size} API file(s)...")
|
||||
|
||||
val violations = mutableMapOf<File, Set<String>>()
|
||||
val excludedRegexes =
|
||||
excludedClassRegexes
|
||||
.map {
|
||||
try {
|
||||
it.toRegex()
|
||||
} catch (ignored: PatternSyntaxException) {
|
||||
throw GradleException("Invalid data exclusion regex: '$it'")
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
|
||||
inputs.files.forEach { apiFile ->
|
||||
logger.lifecycle("Validating public API from file ${apiFile.path}")
|
||||
|
||||
apiFile.useLines { lines ->
|
||||
val actualDataClasses = findDataClasses(lines).filterExclusions(excludedRegexes)
|
||||
|
||||
if (actualDataClasses.isNotEmpty()) {
|
||||
violations[apiFile] = actualDataClasses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.isNotEmpty()) {
|
||||
val message = buildString {
|
||||
appendLine("Data classes found in public API.")
|
||||
appendLine()
|
||||
|
||||
for ((file, dataClasses) in violations.entries) {
|
||||
appendLine("In file ${file.path}:")
|
||||
for (dataClass in dataClasses) {
|
||||
appendLine(" * ${dataClass.replace("/", ".")}")
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
throw GradleException(message)
|
||||
} else {
|
||||
logger.lifecycle("No public API violations found.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun findDataClasses(lines: Sequence<String>): Set<String> {
|
||||
val currentClassStack = Stack<String>()
|
||||
val dataClasses = mutableMapOf<String, DataClassInfo>()
|
||||
|
||||
for (line in lines) {
|
||||
if (line.isBlank()) continue
|
||||
|
||||
val matchResult = classFqnRegex.find(line)
|
||||
if (matchResult != null) {
|
||||
val classFqn = matchResult.groupValues[1]
|
||||
currentClassStack.push(classFqn)
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.contains("}")) {
|
||||
currentClassStack.pop()
|
||||
continue
|
||||
}
|
||||
|
||||
val fqn = currentClassStack.peek()
|
||||
if (copyMethodRegex.find(line) != null) {
|
||||
val info = dataClasses.getOrPut(fqn) { DataClassInfo(fqn) }
|
||||
info.hasCopyMethod = true
|
||||
} else if (line.contains("public static final synthetic fun box-impl")) {
|
||||
val info = dataClasses.getOrPut(fqn) { DataClassInfo(fqn) }
|
||||
info.isLikelyValueClass = true
|
||||
}
|
||||
}
|
||||
|
||||
val actualDataClasses = dataClasses.filterValues { it.hasCopyMethod && !it.isLikelyValueClass }.keys
|
||||
return actualDataClasses
|
||||
}
|
||||
|
||||
private fun Set<String>.filterExclusions(excludedRegexes: Set<Regex>): Set<String> {
|
||||
if (excludedRegexes.isEmpty()) return this
|
||||
|
||||
return filterNot { dataClassFqn ->
|
||||
val isExcluded = excludedRegexes.any { it.matchEntire(dataClassFqn) != null }
|
||||
|
||||
if (isExcluded) {
|
||||
logger.info(" Ignoring excluded data class $dataClassFqn")
|
||||
}
|
||||
isExcluded
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DataClassShouldBeImmutable") // Only used in a loop, saves memory and is faster
|
||||
private data class DataClassInfo(
|
||||
val fqn: String,
|
||||
var hasCopyMethod: Boolean = false,
|
||||
var isLikelyValueClass: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import org.jetbrains.jewel.buildlogic.demodata.AndroidStudioReleasesGeneratorTask
|
||||
import org.jetbrains.jewel.buildlogic.demodata.STUDIO_RELEASES_OUTPUT_CLASS_NAME
|
||||
import org.jetbrains.jewel.buildlogic.demodata.StudioVersionsGenerationExtension
|
||||
|
||||
val extension: StudioVersionsGenerationExtension =
|
||||
extensions.findByType<StudioVersionsGenerationExtension>()
|
||||
?: extensions.create("androidStudioReleasesGenerator", StudioVersionsGenerationExtension::class.java)
|
||||
|
||||
val task =
|
||||
tasks.register<AndroidStudioReleasesGeneratorTask>("generateAndroidStudioReleasesList") {
|
||||
val className = ClassName.bestGuess(STUDIO_RELEASES_OUTPUT_CLASS_NAME)
|
||||
val filePath = className.packageName.replace(".", "/") + "/${className.simpleName}.kt"
|
||||
outputFile = extension.targetDir.file(filePath)
|
||||
dataUrl = extension.dataUrl
|
||||
resourcesDirs = extension.resourcesDirs
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import io.gitlab.arturbosch.detekt.Detekt
|
||||
import org.jetbrains.dokka.gradle.DokkaTask
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
|
||||
import java.lang.reflect.Field
|
||||
import java.net.URLClassLoader
|
||||
|
||||
private val defaultOutputDir: Provider<Directory> = layout.buildDirectory.dir("generated/iconKeys")
|
||||
|
||||
class IconKeysGeneratorContainer(container: NamedDomainObjectContainer<IconKeysGeneration>) :
|
||||
NamedDomainObjectContainer<IconKeysGeneration> by container
|
||||
|
||||
class IconKeysGeneration(val name: String, project: Project) {
|
||||
val outputDirectory: DirectoryProperty = project.objects.directoryProperty().convention(defaultOutputDir)
|
||||
|
||||
val sourceClassName: Property<String> = project.objects.property<String>()
|
||||
val generatedClassName: Property<String> = project.objects.property<String>()
|
||||
}
|
||||
|
||||
val iconGeneration by
|
||||
configurations.registering {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
}
|
||||
|
||||
val extension = IconKeysGeneratorContainer(container<IconKeysGeneration> { IconKeysGeneration(it, project) })
|
||||
|
||||
extensions.add("intelliJIconKeysGenerator", extension)
|
||||
|
||||
extension.all item@{
|
||||
val task =
|
||||
tasks.register<IconKeysGeneratorTask>("generate${name}Keys") task@{
|
||||
this@task.outputDirectory = this@item.outputDirectory
|
||||
this@task.sourceClassName = this@item.sourceClassName
|
||||
this@task.generatedClassName = this@item.generatedClassName
|
||||
configuration.from(iconGeneration)
|
||||
dependsOn(iconGeneration)
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<BaseKotlinCompile> { dependsOn(task) }
|
||||
withType<Detekt> { dependsOn(task) }
|
||||
withType<DokkaTask> { dependsOn(task) }
|
||||
withType<Jar> { dependsOn(task) }
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
|
||||
the<KotlinJvmProjectExtension>().sourceSets["main"].kotlin.srcDir(defaultOutputDir)
|
||||
}
|
||||
|
||||
open class IconKeysGeneratorTask : DefaultTask() {
|
||||
|
||||
@get:OutputDirectory val outputDirectory: DirectoryProperty = project.objects.directoryProperty()
|
||||
|
||||
@get:Input val sourceClassName = project.objects.property<String>()
|
||||
|
||||
@get:Input val generatedClassName = project.objects.property<String>()
|
||||
|
||||
@get:InputFiles val configuration: ConfigurableFileCollection = project.objects.fileCollection()
|
||||
|
||||
init {
|
||||
group = "jewel"
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val guessedSourceClassName =
|
||||
sourceClassName.map { ClassName.bestGuess(it).canonicalName.replace('.', '/') + ".kt" }.get()
|
||||
|
||||
// The icons artifacts are loaded on the iconGeneration configuration's classpath,
|
||||
// so we need a classloader that can access these classes.
|
||||
val classLoader = createClassLoader()
|
||||
val sourceClass =
|
||||
classLoader.loadClass(sourceClassName.get())
|
||||
?: throw GradleException(
|
||||
"Unable to load ${sourceClassName.get()}. " +
|
||||
"Is the correct dependency declared on the iconGeneration configuration?"
|
||||
)
|
||||
|
||||
// Traverse sourceClass by using reflection, collecting all members
|
||||
// This step uses the mappings to add the new paths where available.
|
||||
val dummyIconClass = classLoader.loadClass("com.intellij.ui.DummyIconImpl")
|
||||
val oldUiPathField = dummyIconClass.getOriginalPathField()
|
||||
val newUiPathField = dummyIconClass.getNewUiPathField()
|
||||
|
||||
val rootHolder = IconKeyHolder(sourceClass.simpleName)
|
||||
whileForcingAccessible(oldUiPathField, newUiPathField) {
|
||||
visitSourceClass(sourceClass, rootHolder, oldUiPathField, newUiPathField, classLoader)
|
||||
}
|
||||
logger.lifecycle("Read icon keys from ${sourceClass.name}")
|
||||
|
||||
// Step 4) Generate output Kotlin file
|
||||
val fileSpec = generateKotlinCode(rootHolder)
|
||||
val directory = outputDirectory.get().asFile
|
||||
fileSpec.writeTo(directory)
|
||||
|
||||
logger.lifecycle("Written icon keys for $guessedSourceClassName into $directory")
|
||||
}
|
||||
|
||||
private fun createClassLoader(): URLClassLoader {
|
||||
val arrayOfURLs = configuration.files.map { it.toURI().toURL() }.toTypedArray()
|
||||
|
||||
return URLClassLoader(arrayOfURLs, IconKeysGeneratorTask::class.java.classLoader)
|
||||
}
|
||||
|
||||
private fun whileForcingAccessible(vararg fields: Field, action: () -> Unit) {
|
||||
val wasAccessibles = mutableListOf<Boolean>()
|
||||
for (field in fields) {
|
||||
@Suppress("DEPRECATION")
|
||||
wasAccessibles += field.isAccessible
|
||||
field.isAccessible = true
|
||||
}
|
||||
|
||||
try {
|
||||
action()
|
||||
} finally {
|
||||
for ((index, field) in fields.withIndex()) {
|
||||
field.isAccessible = wasAccessibles[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun visitSourceClass(
|
||||
sourceClass: Class<*>,
|
||||
parentHolder: IconKeyHolder,
|
||||
oldUiPathField: Field,
|
||||
newUiPathField: Field,
|
||||
classLoader: ClassLoader,
|
||||
) {
|
||||
for (child in sourceClass.declaredClasses) {
|
||||
val childName = "${parentHolder.name}.${child.simpleName}"
|
||||
val childHolder = IconKeyHolder(childName)
|
||||
parentHolder.holders += childHolder
|
||||
visitSourceClass(child, childHolder, oldUiPathField, newUiPathField, classLoader)
|
||||
}
|
||||
parentHolder.holders.sortBy { it.name }
|
||||
|
||||
sourceClass.declaredFields
|
||||
.filter { it.type == javax.swing.Icon::class.java }
|
||||
.forEach { field ->
|
||||
val fieldName = "${parentHolder.name}.${field.name}"
|
||||
|
||||
if (field.annotations.any { it.annotationClass == java.lang.Deprecated::class }) {
|
||||
logger.lifecycle("Ignoring deprecated field: $fieldName")
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val icon = field.get(sourceClass)
|
||||
val oldUiPath =
|
||||
oldUiPathField.get(icon) as String? ?: throw GradleException("Found null path in icon $fieldName")
|
||||
validatePath(oldUiPath, fieldName, classLoader)
|
||||
|
||||
// New UI paths may be "partial", meaning they end with a / character.
|
||||
// In this case, we're supposed to append the old UI path to the new UI
|
||||
// path, because that's just how they decided to encode things in IJP.
|
||||
val newUiPath =
|
||||
(newUiPathField.get(icon) as String?)?.let { if (it.endsWith("/")) it + oldUiPath else it }
|
||||
?: oldUiPath
|
||||
validatePath(newUiPath, fieldName, classLoader)
|
||||
parentHolder.keys += IconKey(fieldName, oldUiPath, newUiPath)
|
||||
}
|
||||
parentHolder.keys.sortBy { it.name }
|
||||
}
|
||||
|
||||
private fun validatePath(path: String, fieldName: String, classLoader: ClassLoader) {
|
||||
val iconsClass = classLoader.loadClass(sourceClassName.get())
|
||||
if (iconsClass.getResourceAsStream("/${path.trimStart('/')}") == null) {
|
||||
logger.warn("Icon $fieldName: '$path' does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateKotlinCode(rootHolder: IconKeyHolder): FileSpec {
|
||||
val className = ClassName.bestGuess(generatedClassName.get())
|
||||
|
||||
return FileSpec.builder(className)
|
||||
.apply {
|
||||
indent(" ")
|
||||
addFileComment("Generated by the Jewel icon keys generator\n")
|
||||
addFileComment("Source class: ${sourceClassName.get()}")
|
||||
|
||||
addImport(keyClassName.packageName, keyClassName.simpleName)
|
||||
|
||||
val objectName = ClassName.bestGuess(generatedClassName.get())
|
||||
addType(
|
||||
TypeSpec.objectBuilder(objectName)
|
||||
.apply {
|
||||
for (childHolder in rootHolder.holders) {
|
||||
generateKotlinCodeInner(childHolder, className)
|
||||
}
|
||||
|
||||
for (key in rootHolder.keys) {
|
||||
addProperty(buildIconKeyEntry(key, className))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun TypeSpec.Builder.generateKotlinCodeInner(holder: IconKeyHolder, rootClassName: ClassName) {
|
||||
val objectName = holder.name.substringAfterLast('.')
|
||||
addType(
|
||||
TypeSpec.objectBuilder(objectName)
|
||||
.apply {
|
||||
for (childHolder in holder.holders) {
|
||||
generateKotlinCodeInner(childHolder, rootClassName)
|
||||
}
|
||||
|
||||
for (key in holder.keys) {
|
||||
addProperty(buildIconKeyEntry(key, rootClassName))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildIconKeyEntry(key: IconKey, rootClassName: ClassName) =
|
||||
PropertySpec.builder(key.name.substringAfterLast('.'), keyClassName)
|
||||
.initializer(
|
||||
"%L",
|
||||
"IntelliJIconKey(\"${key.oldPath}\", " +
|
||||
"\"${key.newPath ?: key.oldPath}\", " +
|
||||
"${rootClassName.simpleName}::class.java)",
|
||||
)
|
||||
.build()
|
||||
|
||||
companion object {
|
||||
|
||||
private fun Class<*>.getOriginalPathField(): Field = declaredFields.first { it.name == "originalPath" }
|
||||
|
||||
private fun Class<*>.getNewUiPathField(): Field = declaredFields.first { it.name == "expUIPath" }
|
||||
|
||||
private val keyClassName = ClassName("org.jetbrains.jewel.ui.icon", "IntelliJIconKey")
|
||||
}
|
||||
}
|
||||
|
||||
private data class IconKeyHolder(
|
||||
val name: String,
|
||||
val holders: MutableList<IconKeyHolder> = mutableListOf(),
|
||||
val keys: MutableList<IconKey> = mutableListOf(),
|
||||
)
|
||||
|
||||
private data class IconKey(val name: String, val oldPath: String, val newPath: String?)
|
||||
@@ -0,0 +1,3 @@
|
||||
import org.jetbrains.jewel.buildlogic.ideversion.CheckIdeaVersionTask
|
||||
|
||||
tasks.register<CheckIdeaVersionTask>("checkLatestIntelliJPlatformBuild")
|
||||
@@ -0,0 +1,42 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import io.gitlab.arturbosch.detekt.Detekt
|
||||
import org.gradle.util.internal.GUtil
|
||||
import org.jetbrains.dokka.gradle.DokkaTask
|
||||
import org.jetbrains.jewel.buildlogic.theme.IntelliJThemeGeneratorTask
|
||||
import org.jetbrains.jewel.buildlogic.theme.ThemeGeneration
|
||||
import org.jetbrains.jewel.buildlogic.theme.ThemeGeneratorContainer
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
|
||||
|
||||
val extension = ThemeGeneratorContainer(container<ThemeGeneration> { ThemeGeneration(it, project) })
|
||||
|
||||
extensions.add("intelliJThemeGenerator", extension)
|
||||
|
||||
extension.all {
|
||||
val task =
|
||||
tasks.register<IntelliJThemeGeneratorTask>("generate${GUtil.toCamelCase(name)}Theme") {
|
||||
val paths =
|
||||
this@all.themeClassName.map {
|
||||
val className = ClassName.bestGuess(it)
|
||||
className.packageName.replace(".", "/") + "/${className.simpleName}.kt"
|
||||
}
|
||||
|
||||
outputFile = targetDir.file(paths)
|
||||
themeClassName = this@all.themeClassName
|
||||
ideaVersion = this@all.ideaVersion
|
||||
themeFile = this@all.themeFile
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<BaseKotlinCompile> { dependsOn(task) }
|
||||
withType<Detekt> { dependsOn(task) }
|
||||
withType<DokkaTask> { dependsOn(task) }
|
||||
withType<Jar> { dependsOn(task) }
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
|
||||
the<KotlinJvmProjectExtension>().sourceSets["main"].kotlin.srcDir(targetDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import org.jetbrains.jewel.buildlogic.apivalidation.ApiValidationExtension
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.kotlinx.binary-compatibility-validator")
|
||||
id("dev.drewhamilton.poko")
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
apiValidation {
|
||||
/**
|
||||
* Set of annotations that exclude API from being public. Typically, it is all kinds of `@InternalApi` annotations
|
||||
* that mark effectively private API that cannot be actually private for technical reasons.
|
||||
*/
|
||||
nonPublicMarkers.add("org.jetbrains.jewel.InternalJewelApi")
|
||||
}
|
||||
|
||||
poko { pokoAnnotation = "org/jetbrains/jewel/foundation/GenerateDataFunctions" }
|
||||
|
||||
kotlin { explicitApi() }
|
||||
|
||||
val extension = project.extensions.create("publicApiValidation", ApiValidationExtension::class.java)
|
||||
|
||||
with(extension) { excludedClassRegexes.convention(emptySet()) }
|
||||
|
||||
tasks {
|
||||
val validatePublicApi =
|
||||
register<ValidatePublicApiTask>("validatePublicApi") {
|
||||
include { it.file.extension == "api" }
|
||||
source(project.fileTree("api"))
|
||||
dependsOn(named("apiCheck"))
|
||||
excludedClassRegexes = project.the<ApiValidationExtension>().excludedClassRegexes.get()
|
||||
}
|
||||
|
||||
named("check") { dependsOn(validatePublicApi) }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("org.jmailen.kotlinter")
|
||||
id("com.ncorti.ktfmt.gradle")
|
||||
}
|
||||
|
||||
configurations {
|
||||
val dependencies = register("sarif") { isCanBeDeclared = true }
|
||||
register("outgoingSarif") {
|
||||
isCanBeConsumed = true
|
||||
isCanBeResolved = true
|
||||
extendsFrom(dependencies.get())
|
||||
attributes { attribute(Usage.USAGE_ATTRIBUTE, objects.named("sarif")) }
|
||||
}
|
||||
}
|
||||
|
||||
ktfmt {
|
||||
maxWidth = 120
|
||||
blockIndent = 4
|
||||
continuationIndent = 4
|
||||
manageTrailingCommas = true
|
||||
removeUnusedImports = true
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
`maven-publish`
|
||||
id("org.jetbrains.dokka")
|
||||
signing
|
||||
}
|
||||
|
||||
val sourcesJar by
|
||||
tasks.registering(Jar::class) {
|
||||
from(kotlin.sourceSets.main.map { it.kotlin })
|
||||
archiveClassifier = "sources"
|
||||
destinationDirectory = layout.buildDirectory.dir("artifacts")
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
val javadocJar by
|
||||
tasks.registering(Jar::class) {
|
||||
from(tasks.dokkaHtml)
|
||||
archiveClassifier = "javadoc"
|
||||
destinationDirectory = layout.buildDirectory.dir("artifacts")
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
val publishingExtension = extensions.getByType<PublishingExtension>()
|
||||
|
||||
signing {
|
||||
useInMemoryPgpKeys(
|
||||
System.getenv("PGP_PRIVATE_KEY") ?: properties["signing.privateKey"] as String?,
|
||||
System.getenv("PGP_PASSWORD") ?: properties["signing.password"] as String?,
|
||||
)
|
||||
|
||||
if (project.hasProperty("no-sign")) {
|
||||
logger.warn("⚠️ CAUTION! NO-SIGN MODE ENABLED, PUBLICATIONS WON'T BE SIGNED")
|
||||
} else {
|
||||
sign(publishingExtension.publications)
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
configureJewelRepositories(project)
|
||||
|
||||
val ijpTarget = project.property("ijp.target") as String
|
||||
|
||||
publications {
|
||||
register<MavenPublication>("main") {
|
||||
from(components["kotlin"])
|
||||
artifact(javadocJar)
|
||||
artifact(sourcesJar)
|
||||
version = project.version as String
|
||||
artifactId = "jewel-${project.name}-$ijpTarget"
|
||||
pom { configureJewelPom() }
|
||||
}
|
||||
}
|
||||
}
|
||||
98
platform/jewel/buildSrc/src/main/kotlin/jewel.gradle.kts
Normal file
98
platform/jewel/buildSrc/src/main/kotlin/jewel.gradle.kts
Normal file
@@ -0,0 +1,98 @@
|
||||
import com.ncorti.ktfmt.gradle.tasks.KtfmtBaseTask
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("jewel-linting")
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
group = "org.jetbrains.jewel"
|
||||
|
||||
val gitHubRef: String? = System.getenv("GITHUB_REF")
|
||||
|
||||
version =
|
||||
when {
|
||||
properties.containsKey("versionOverride") -> {
|
||||
val rawVersion = (properties["versionOverride"] as String).trim()
|
||||
if (!rawVersion.matches("^\\d\\.\\d{2,}\\.\\d+$".toRegex())) {
|
||||
throw GradleException("Invalid versionOverride: $rawVersion")
|
||||
}
|
||||
logger.warn("Using version override: $rawVersion")
|
||||
rawVersion
|
||||
}
|
||||
gitHubRef?.startsWith("refs/tags/") == true -> {
|
||||
gitHubRef.substringAfter("refs/tags/").removePrefix("v")
|
||||
}
|
||||
properties.containsKey("useCurrentVersion") -> {
|
||||
val rawVersion = (properties["jewel.release.version"] as String).trim()
|
||||
if (!rawVersion.matches("^\\d\\.\\d{2,}\\.\\d+$".toRegex())) {
|
||||
throw GradleException("Invalid jewel.release.version found in gradle.properties: $rawVersion")
|
||||
}
|
||||
logger.warn("Using jewel.release.version: $rawVersion")
|
||||
rawVersion
|
||||
}
|
||||
|
||||
else -> "1.0.0-SNAPSHOT"
|
||||
}
|
||||
|
||||
val jdkLevel = project.property("jdk.level") as String
|
||||
|
||||
kotlin {
|
||||
jvmToolchain { languageVersion = JavaLanguageVersion.of(jdkLevel) }
|
||||
|
||||
compilerOptions.jvmTarget.set(JvmTarget.fromTarget(jdkLevel))
|
||||
|
||||
target {
|
||||
compilations.all { kotlinOptions { freeCompilerArgs += "-Xcontext-receivers" } }
|
||||
sourceSets.all {
|
||||
languageSettings {
|
||||
optIn("androidx.compose.foundation.ExperimentalFoundationApi")
|
||||
optIn("androidx.compose.ui.ExperimentalComposeUiApi")
|
||||
optIn("kotlin.experimental.ExperimentalTypeInference")
|
||||
optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
optIn("org.jetbrains.jewel.foundation.ExperimentalJewelApi")
|
||||
optIn("org.jetbrains.jewel.foundation.InternalJewelApi")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
detekt {
|
||||
config.from(files(rootProject.file("detekt.yml")))
|
||||
buildUponDefaultConfig = true
|
||||
}
|
||||
|
||||
val sarifReport: Provider<RegularFile> = layout.buildDirectory.file("reports/ktlint-${project.name}.sarif")
|
||||
|
||||
tasks {
|
||||
detektMain {
|
||||
val sarifOutputFile = layout.buildDirectory.file("reports/detekt-${project.name}.sarif")
|
||||
exclude { it.file.absolutePath.startsWith(layout.buildDirectory.asFile.get().absolutePath) }
|
||||
reports {
|
||||
sarif.required = true
|
||||
sarif.outputLocation = sarifOutputFile
|
||||
}
|
||||
}
|
||||
|
||||
formatKotlinMain { exclude { it.file.absolutePath.replace('\\', '/').contains("build/generated") } }
|
||||
withType<KtfmtBaseTask> { exclude { it.file.absolutePath.contains("build/generated") } }
|
||||
|
||||
lintKotlinMain {
|
||||
exclude { it.file.absolutePath.replace('\\', '/').contains("build/generated") }
|
||||
|
||||
reports = provider {
|
||||
mapOf(
|
||||
"plain" to layout.buildDirectory.file("reports/ktlint-${project.name}.txt").get().asFile,
|
||||
"html" to layout.buildDirectory.file("reports/ktlint-${project.name}.html").get().asFile,
|
||||
"sarif" to sarifReport.get().asFile,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations.named("sarif") {
|
||||
outgoing {
|
||||
artifact(tasks.detektMain.flatMap { it.sarifReportFile }) { builtBy(tasks.detektMain) }
|
||||
artifact(sarifReport) { builtBy(tasks.lintKotlinMain) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.jetbrains.jewel.buildlogic.apivalidation
|
||||
|
||||
import org.gradle.api.provider.SetProperty
|
||||
|
||||
interface ApiValidationExtension {
|
||||
|
||||
val excludedClassRegexes: SetProperty<String>
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.jetbrains.jewel.buildlogic.demodata
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import gradle.kotlin.dsl.accessors._327d2b3378ed6d2c1bec5d20438f90c7.kotlin
|
||||
import gradle.kotlin.dsl.accessors._327d2b3378ed6d2c1bec5d20438f90c7.sourceSets
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.SourceSetContainer
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.kotlin.dsl.get
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.property
|
||||
import org.gradle.kotlin.dsl.setProperty
|
||||
|
||||
open class StudioVersionsGenerationExtension(project: Project) {
|
||||
|
||||
val targetDir: DirectoryProperty =
|
||||
project.objects
|
||||
.directoryProperty()
|
||||
.convention(
|
||||
project.layout.dir(project.provider { project.sourceSets.named("main").get().kotlin.srcDirs.first() })
|
||||
)
|
||||
|
||||
val resourcesDirs: SetProperty<File> =
|
||||
project.objects
|
||||
.setProperty<File>()
|
||||
.convention(
|
||||
project.provider {
|
||||
when {
|
||||
project.plugins.hasPlugin("org.gradle.jvm-ecosystem") ->
|
||||
project.extensions.getByType<SourceSetContainer>()["main"].resources.srcDirs
|
||||
|
||||
else -> emptySet()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val dataUrl: Property<String> =
|
||||
project.objects.property<String>().convention("https://jb.gg/android-studio-releases-list.json")
|
||||
}
|
||||
|
||||
internal const val STUDIO_RELEASES_OUTPUT_CLASS_NAME =
|
||||
"org.jetbrains.jewel.samples.ideplugin.releasessample.AndroidStudioReleases"
|
||||
|
||||
open class AndroidStudioReleasesGeneratorTask : DefaultTask() {
|
||||
|
||||
@get:OutputFile val outputFile: RegularFileProperty = project.objects.fileProperty()
|
||||
|
||||
@get:Input val dataUrl = project.objects.property<String>()
|
||||
|
||||
@get:Input val resourcesDirs = project.objects.setProperty<File>()
|
||||
|
||||
init {
|
||||
group = "jewel"
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
}
|
||||
val url = dataUrl.get()
|
||||
val lookupDirs = resourcesDirs.get()
|
||||
|
||||
logger.lifecycle("Fetching Android Studio releases list from $url...")
|
||||
logger.debug("Registered resources directories:\n" + lookupDirs.joinToString("\n") { " * ${it.absolutePath}" })
|
||||
val releases = URI.create(url).toURL().openStream().use { json.decodeFromStream<ApiAndroidStudioReleases>(it) }
|
||||
|
||||
val className = ClassName.bestGuess(STUDIO_RELEASES_OUTPUT_CLASS_NAME)
|
||||
val file = AndroidStudioReleasesReader.readFrom(releases, className, url, lookupDirs)
|
||||
|
||||
val outputFile = outputFile.get().asFile
|
||||
outputFile.bufferedWriter().use { file.writeTo(it) }
|
||||
logger.lifecycle("Android Studio releases from $url parsed and code generated into ${outputFile.path}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.jetbrains.jewel.buildlogic.demodata
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.joinToCode
|
||||
import java.io.File
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
private val ContentItemClassName =
|
||||
ClassName.bestGuess("org.jetbrains.jewel.samples.ideplugin.releasessample.ContentItem.AndroidStudio")
|
||||
|
||||
internal object AndroidStudioReleasesReader {
|
||||
|
||||
fun readFrom(releases: ApiAndroidStudioReleases, className: ClassName, url: String, resourceDirs: Set<File>) =
|
||||
FileSpec.builder(className)
|
||||
.apply {
|
||||
indent(" ")
|
||||
addFileComment("Generated by the Jewel Android Studio Releases Generator\n")
|
||||
addFileComment("Generated from $url on ${ZonedDateTime.now()}")
|
||||
|
||||
addImport("org.jetbrains.jewel.samples.ideplugin.releasessample", "ContentItem.AndroidStudio")
|
||||
addImport("kotlinx.datetime", "LocalDate")
|
||||
|
||||
addType(createBaseTypeSpec(className, releases, resourceDirs))
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun createBaseTypeSpec(className: ClassName, releases: ApiAndroidStudioReleases, resourceDirs: Set<File>) =
|
||||
TypeSpec.objectBuilder(className)
|
||||
.superclass(
|
||||
ClassName.bestGuess("org.jetbrains.jewel.samples.ideplugin.releasessample.ContentSource")
|
||||
.parameterizedBy(ContentItemClassName)
|
||||
)
|
||||
.apply {
|
||||
addProperty(
|
||||
PropertySpec.builder(
|
||||
name = "items",
|
||||
type = List::class.asClassName().parameterizedBy(ContentItemClassName),
|
||||
KModifier.OVERRIDE,
|
||||
)
|
||||
.initializer(readReleases(releases, resourceDirs))
|
||||
.build()
|
||||
)
|
||||
|
||||
addProperty(
|
||||
PropertySpec.builder("displayName", type = String::class.asClassName(), KModifier.OVERRIDE)
|
||||
.initializer("\"%L\"", "Android Studio releases")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun readReleases(releases: ApiAndroidStudioReleases, resourceDirs: Set<File>) =
|
||||
releases.content.item
|
||||
.map { readRelease(it, resourceDirs) }
|
||||
.joinToCode(prefix = "listOf(\n", separator = ",\n", suffix = ")")
|
||||
|
||||
private fun readRelease(release: ApiAndroidStudioReleases.Content.Item, resourceDirs: Set<File>) =
|
||||
CodeBlock.builder()
|
||||
.apply {
|
||||
add("AndroidStudio(\n")
|
||||
add(" displayText = \"%L\",\n", release.name)
|
||||
add(" imagePath = %L,\n", imagePathForOrNull(release, resourceDirs))
|
||||
add(" versionName = \"%L\",\n", release.version)
|
||||
add(" build = \"%L\",\n", release.build)
|
||||
add(" platformBuild = \"%L\",\n", release.platformBuild)
|
||||
add(" platformVersion = \"%L\",\n", release.platformVersion)
|
||||
add(" channel = %L,\n", readChannel(release.channel))
|
||||
add(" releaseDate = LocalDate(%L),\n", translateDate(release.date))
|
||||
add(" key = \"%L\",\n", release.build)
|
||||
add(")")
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun imagePathForOrNull(release: ApiAndroidStudioReleases.Content.Item, resourceDirs: Set<File>): String? {
|
||||
// Take the release animal from the name, remove spaces and voila'
|
||||
val releaseAnimal = release.name.substringBefore(" | ").substringAfter("Android Studio").trim().replace(" ", "")
|
||||
|
||||
if (releaseAnimal.isEmpty() || releaseAnimal.any { it.isDigit() }) return null
|
||||
|
||||
// We only have stable and canary splash screens. Betas use the stable ones.
|
||||
val channel =
|
||||
release.channel.lowercase().let {
|
||||
when (it) {
|
||||
"release",
|
||||
"rc",
|
||||
"stable",
|
||||
"beta",
|
||||
"patch" -> "stable"
|
||||
"canary",
|
||||
"preview",
|
||||
"alpha" -> "canary"
|
||||
else -> {
|
||||
println(" Note: channel '${it}' isn't supported for splash screens")
|
||||
null
|
||||
}
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
val splashPath = "/studio-splash-screens/$releaseAnimal-$channel.png"
|
||||
val splashFiles = resourceDirs.map { dir -> File(dir, splashPath) }
|
||||
if (splashFiles.none { it.isFile }) {
|
||||
println(" Note: expected splash screen file doesn't exist: '${splashPath}'")
|
||||
return null
|
||||
}
|
||||
|
||||
return "\"$splashPath\""
|
||||
}
|
||||
|
||||
// This is the laziest crap ever, I am sorry.
|
||||
private fun translateDate(rawDate: String): String {
|
||||
val month = rawDate.substringBefore(" ").trimStart('0')
|
||||
val year = rawDate.substringAfterLast(" ".trimStart('0'))
|
||||
val day = rawDate.substring(month.length + 1, rawDate.length - year.length - 1).trimStart('0')
|
||||
|
||||
if (day.isEmpty()) {
|
||||
println("$rawDate\nMonth: '$month'\nYear: '$year'")
|
||||
}
|
||||
|
||||
val monthNumber =
|
||||
when (month.trim().lowercase()) {
|
||||
"january" -> 1
|
||||
"february" -> 2
|
||||
"march" -> 3
|
||||
"april" -> 4
|
||||
"may" -> 5
|
||||
"june" -> 6
|
||||
"july" -> 7
|
||||
"august" -> 8
|
||||
"september" -> 9
|
||||
"october" -> 10
|
||||
"november" -> 11
|
||||
"december" -> 12
|
||||
else -> error("Unrecognized month: $month")
|
||||
}
|
||||
|
||||
return "$year, $monthNumber, $day"
|
||||
}
|
||||
|
||||
private fun readChannel(rawChannel: String) =
|
||||
when (rawChannel.lowercase().trim()) {
|
||||
"stable",
|
||||
"patch",
|
||||
"release" -> "ReleaseChannel.Stable"
|
||||
"beta" -> "ReleaseChannel.Beta"
|
||||
"canary" -> "ReleaseChannel.Canary"
|
||||
else -> "ReleaseChannel.Other"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.jetbrains.jewel.buildlogic.demodata
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class ApiAndroidStudioReleases(@SerialName("content") val content: Content = Content()) {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("item") val item: List<Item> = listOf(),
|
||||
@SerialName("version") val version: Int = 0,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
internal data class Item(
|
||||
@SerialName("build") val build: String,
|
||||
@SerialName("channel") val channel: String,
|
||||
@SerialName("date") val date: String,
|
||||
@SerialName("download") val download: List<Download> = listOf(),
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("platformBuild") val platformBuild: String,
|
||||
@SerialName("platformVersion") val platformVersion: String,
|
||||
@SerialName("version") val version: String,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
internal data class Download(
|
||||
@SerialName("checksum") val checksum: String,
|
||||
@SerialName("link") val link: String,
|
||||
@SerialName("size") val size: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.jetbrains.jewel.buildlogic.ideversion
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class ApiIdeaReleasesItem(
|
||||
@SerialName("code") val code: String,
|
||||
@SerialName("releases") val releases: List<Release>,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
internal data class Release(
|
||||
@SerialName("build") val build: String,
|
||||
@SerialName("type") val type: String,
|
||||
@SerialName("version") val version: String,
|
||||
@SerialName("majorVersion") val majorVersion: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package org.jetbrains.jewel.buildlogic.ideversion
|
||||
|
||||
import java.io.File
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class CheckIdeaVersionTask : DefaultTask() {
|
||||
|
||||
private val releasesUrl =
|
||||
"https://data.services.jetbrains.com/products?" +
|
||||
"fields=code,releases,releases.version,releases.build,releases.type,releases.majorVersion&" +
|
||||
"code=IC"
|
||||
|
||||
private val ideaVersionRegex = "202\\d\\.(\\d\\.)?\\d".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private val intelliJPlatformBuildRegex = "2\\d{2}\\.\\d+\\.\\d+(?:-EAP-SNAPSHOT)?".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private val currentIjpVersion = project.currentIjpVersion
|
||||
|
||||
init {
|
||||
group = "jewel"
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
logger.lifecycle("Fetching IntelliJ Platform releases from $releasesUrl...")
|
||||
val ideaVersion = readCurrentVersionInfo()
|
||||
validateIdeaVersion(ideaVersion)
|
||||
|
||||
val platformBuildsForThisMajorVersion =
|
||||
IJPVersionsFetcher.fetchBuildsForCurrentMajorVersion(releasesUrl, ideaVersion.majorVersion, logger)
|
||||
|
||||
if (platformBuildsForThisMajorVersion == null) {
|
||||
logger.error("Cannot check platform version, no builds found for current version $ideaVersion")
|
||||
return
|
||||
}
|
||||
|
||||
val latestAvailableBuild = platformBuildsForThisMajorVersion.last()
|
||||
logger.info("The latest IntelliJ Platform ${ideaVersion.version} build is ${latestAvailableBuild.build}")
|
||||
|
||||
val isCurrentBuildStable = ideaVersion.type.lowercase() != "eap"
|
||||
if (IJPVersionsFetcher.compare(ideaVersion, latestAvailableBuild) < 0) {
|
||||
throw GradleException(
|
||||
buildString {
|
||||
appendLine("IntelliJ Platform version dependency is out of date.")
|
||||
appendLine()
|
||||
|
||||
append("Current build: ${ideaVersion.build}")
|
||||
if (!isCurrentBuildStable) append("-EAP-SNAPSHOT")
|
||||
appendLine()
|
||||
appendLine("Current version: ${ideaVersion.version}")
|
||||
append("Detected channel: ")
|
||||
appendLine(latestAvailableBuild.type)
|
||||
appendLine()
|
||||
|
||||
append("Latest build: ${latestAvailableBuild.build}")
|
||||
if (!isCurrentBuildStable) append("-EAP-SNAPSHOT")
|
||||
appendLine()
|
||||
|
||||
append("Latest version: ")
|
||||
if (isCurrentBuildStable) {
|
||||
appendLine(latestAvailableBuild.version)
|
||||
} else {
|
||||
appendLine(latestAvailableBuild.build.removeSuffix("-EAP-SNAPSHOT"))
|
||||
}
|
||||
|
||||
appendLine(
|
||||
"Please update the 'idea' and 'intelliJPlatformBuild' " + "versions in the catalog accordingly."
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
logger.lifecycle(
|
||||
"No IntelliJ Platform version updates available. " +
|
||||
"Current: ${ideaVersion.build} (${ideaVersion.version})"
|
||||
)
|
||||
}
|
||||
|
||||
private fun readCurrentVersionInfo(): ApiIdeaReleasesItem.Release {
|
||||
val catalogFile = project.rootProject.file("gradle/libs.versions.toml")
|
||||
val ideaVersion = readIdeaVersion(catalogFile)
|
||||
val isStableBuild = !ideaVersion.matches(intelliJPlatformBuildRegex)
|
||||
|
||||
val platformBuild = readPlatformBuild(catalogFile)
|
||||
val majorVersion =
|
||||
if (isStableBuild) {
|
||||
asMajorPlatformVersion(ideaVersion)
|
||||
} else {
|
||||
inferMajorPlatformVersion(platformBuild)
|
||||
}
|
||||
|
||||
return ApiIdeaReleasesItem.Release(
|
||||
build = platformBuild.removeSuffix("-EAP-SNAPSHOT"),
|
||||
version = ideaVersion,
|
||||
majorVersion = majorVersion,
|
||||
type = if (isStableBuild) "release" else "eap",
|
||||
)
|
||||
}
|
||||
|
||||
private fun asMajorPlatformVersion(rawVersion: String) = rawVersion.take(6)
|
||||
|
||||
private fun inferMajorPlatformVersion(rawBuildNumber: String) =
|
||||
"20${rawBuildNumber.take(2)}.${rawBuildNumber.substringBefore('.').last()}"
|
||||
|
||||
private fun readIdeaVersion(catalogFile: File): String {
|
||||
val versionName = "idea"
|
||||
|
||||
val catalogDependencyLine =
|
||||
catalogFile.useLines { lines -> lines.find { it.startsWith(versionName) } }
|
||||
?: throw GradleException(
|
||||
"Unable to find IJP dependency '$versionName' in the catalog file '${catalogFile.path}'"
|
||||
)
|
||||
|
||||
val dependencyVersion =
|
||||
catalogDependencyLine.substringAfter(versionName).trimStart(' ', '=').trimEnd().trim('"')
|
||||
|
||||
if (!dependencyVersion.matches(ideaVersionRegex) && !dependencyVersion.matches(intelliJPlatformBuildRegex)) {
|
||||
throw GradleException("Invalid IJ IDEA version found in version catalog: '$dependencyVersion'")
|
||||
}
|
||||
|
||||
return dependencyVersion
|
||||
}
|
||||
|
||||
private fun readPlatformBuild(catalogFile: File): String {
|
||||
val versionName = "intelliJPlatformBuild"
|
||||
|
||||
val catalogDependencyLine =
|
||||
catalogFile.useLines { lines -> lines.find { it.startsWith(versionName) } }
|
||||
?: throw GradleException(
|
||||
"Unable to find IJP dependency '$versionName' in the catalog file '${catalogFile.path}'"
|
||||
)
|
||||
|
||||
val declaredPlatformBuild =
|
||||
catalogDependencyLine.substringAfter(versionName).trimStart(' ', '=').trimEnd().trim('"')
|
||||
|
||||
if (!declaredPlatformBuild.matches(intelliJPlatformBuildRegex)) {
|
||||
throw GradleException("Invalid IJP build found in version catalog: '$declaredPlatformBuild'")
|
||||
}
|
||||
|
||||
return declaredPlatformBuild
|
||||
}
|
||||
|
||||
private fun validateIdeaVersion(currentVersion: ApiIdeaReleasesItem.Release) {
|
||||
val candidateMatches =
|
||||
IJPVersionsFetcher.fetchIJPVersions(releasesUrl, logger)
|
||||
?: throw GradleException("Can't fetch all IJP releases.")
|
||||
|
||||
val match =
|
||||
candidateMatches.find { it.build == currentVersion.build }
|
||||
?: throw GradleException("IJ build ${currentVersion.build} seemingly does not exist")
|
||||
|
||||
if (currentVersion.type != "eap" && match.version != currentVersion.version) {
|
||||
throw GradleException(
|
||||
buildString {
|
||||
appendLine("The 'idea' and 'intelliJPlatformBuild' properties in the catalog don't match.")
|
||||
append("'idea' = ")
|
||||
append(currentVersion.version)
|
||||
append(", 'intelliJPlatformBuild' = ")
|
||||
appendLine(currentVersion.build)
|
||||
appendLine()
|
||||
appendLine("That build number is for version ${match.version}.")
|
||||
appendLine("Adjust the values so they're aligned correctly.")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// The match's build doesn't contain the -EAP-SNAPSHOT SUFFIX
|
||||
if (currentVersion.type == "eap" && currentVersion.version != match.build) {
|
||||
throw GradleException(
|
||||
buildString {
|
||||
appendLine("The 'idea' and 'intelliJPlatformBuild' properties in the catalog don't match.")
|
||||
append("'idea' = ")
|
||||
append(currentVersion.version)
|
||||
append(", 'intelliJPlatformBuild' = ")
|
||||
appendLine(currentVersion.build + "-EAP-SNAPSHOT")
|
||||
appendLine()
|
||||
appendLine("For non-stable IJP versions, the version and build should match,")
|
||||
appendLine("minus the '-EAP-SNAPSHOT' suffix in the build number.")
|
||||
appendLine("Adjust the values so they're aligned correctly.")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jetbrains.jewel.buildlogic.ideversion
|
||||
|
||||
import org.gradle.api.Project
|
||||
|
||||
val Project.currentIjpVersion: String
|
||||
get() {
|
||||
val rawValue =
|
||||
property("ijp.target") as? String ?: error("Property ijp.target not defined. Check your gradle.properties!")
|
||||
|
||||
if (rawValue.length != 3 || rawValue.toIntOrNull()?.let { it < 0 } == true) {
|
||||
error("Invalid ijp.target property value: '$rawValue'")
|
||||
}
|
||||
|
||||
return rawValue
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.jetbrains.jewel.buildlogic.ideversion
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import org.gradle.api.logging.Logger
|
||||
|
||||
internal object IJPVersionsFetcher {
|
||||
|
||||
fun fetchIJPVersions(releasesUrl: String, logger: Logger): List<ApiIdeaReleasesItem.Release>? {
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
}
|
||||
|
||||
val icReleases =
|
||||
try {
|
||||
URI.create(releasesUrl)
|
||||
.toURL()
|
||||
.openStream()
|
||||
.use { json.decodeFromStream<List<ApiIdeaReleasesItem>>(it) }
|
||||
.first()
|
||||
} catch (e: IOException) {
|
||||
logger.warn(
|
||||
"Couldn't fetch IJ Platform releases, can't check for updates.\n" +
|
||||
"Cause: ${e::class.qualifiedName} — ${e.message}"
|
||||
)
|
||||
return null
|
||||
} catch (e: RuntimeException) {
|
||||
logger.error("Unexpected error while fetching IJ Platform releases, can't check for updates.", e)
|
||||
return null
|
||||
}
|
||||
|
||||
check(icReleases.code == "IIC") { "Was expecting code IIC but was ${icReleases.code}" }
|
||||
check(icReleases.releases.isNotEmpty()) { "Was expecting to have releases but the list is empty" }
|
||||
|
||||
return icReleases.releases
|
||||
}
|
||||
|
||||
fun fetchBuildsForCurrentMajorVersion(
|
||||
releasesUrl: String,
|
||||
majorPlatformVersion: String,
|
||||
logger: Logger,
|
||||
): List<ApiIdeaReleasesItem.Release>? {
|
||||
val releases = fetchIJPVersions(releasesUrl, logger) ?: return null
|
||||
|
||||
return releases
|
||||
.asSequence()
|
||||
.filter { it.majorVersion == majorPlatformVersion }
|
||||
.sortedWith(ReleaseComparator)
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun fetchLatestBuildForCurrentMajorVersion(releasesUrl: String, majorPlatformVersion: String, logger: Logger) =
|
||||
fetchBuildsForCurrentMajorVersion(releasesUrl, majorPlatformVersion, logger)?.last()
|
||||
|
||||
fun compare(first: ApiIdeaReleasesItem.Release, second: ApiIdeaReleasesItem.Release): Int =
|
||||
VersionComparator.compare(first.build, second.build)
|
||||
|
||||
private object ReleaseComparator : Comparator<ApiIdeaReleasesItem.Release> {
|
||||
|
||||
override fun compare(o1: ApiIdeaReleasesItem.Release?, o2: ApiIdeaReleasesItem.Release?): Int {
|
||||
if (o1 == o2) return 0
|
||||
if (o1 == null) return -1
|
||||
if (o2 == null) return 1
|
||||
|
||||
return VersionComparator.compare(o1.build, o2.build)
|
||||
}
|
||||
}
|
||||
|
||||
private object VersionComparator : Comparator<String> {
|
||||
|
||||
override fun compare(o1: String?, o2: String?): Int {
|
||||
if (o1 == o2) return 0
|
||||
if (o1 == null) return -1
|
||||
if (o2 == null) return 1
|
||||
|
||||
require(o1.isNotEmpty() && o1.all { it.isDigit() || it == '.' }) { "The first version is invalid: '$o1'" }
|
||||
require(o2.isNotEmpty() && o2.all { it.isDigit() || it == '.' }) { "The first version is invalid: '$o2'" }
|
||||
|
||||
val firstGroups = o1.split('.')
|
||||
val secondGroups = o2.split('.')
|
||||
|
||||
require(firstGroups.size == 3) { "The first version is invalid: '$o1'" }
|
||||
require(secondGroups.size == 3) { "The second version is invalid: '$o2'" }
|
||||
|
||||
val firstComparison = firstGroups[0].toInt().compareTo(secondGroups[0].toInt())
|
||||
if (firstComparison != 0) return firstComparison
|
||||
|
||||
val secondComparison = firstGroups[1].toInt().compareTo(secondGroups[1].toInt())
|
||||
if (secondComparison != 0) return secondComparison
|
||||
|
||||
return firstGroups[2].toInt().compareTo(secondGroups[2].toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.jetbrains.jewel.buildlogic.theme
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import com.squareup.kotlinpoet.joinToCode
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
internal object IntUiThemeDescriptorReader {
|
||||
|
||||
private val colorGroups = setOf("Gray", "Blue", "Green", "Red", "Yellow", "Orange", "Purple", "Teal")
|
||||
private val colorClassName = ClassName("androidx.compose.ui.graphics", "Color")
|
||||
|
||||
fun readThemeFrom(
|
||||
themeDescriptor: IntellijThemeDescriptor,
|
||||
className: ClassName,
|
||||
ideaVersion: String,
|
||||
descriptorUrl: String,
|
||||
) =
|
||||
FileSpec.builder(className)
|
||||
.apply {
|
||||
indent(" ")
|
||||
addFileComment("Generated by the Jewel Int UI Palette Generator\n")
|
||||
addFileComment("Generated from the IntelliJ Platform version $ideaVersion\n")
|
||||
addFileComment("Source: $descriptorUrl")
|
||||
|
||||
addImport(colorClassName.packageName, colorClassName.simpleName)
|
||||
|
||||
addType(
|
||||
TypeSpec.objectBuilder(className)
|
||||
.apply {
|
||||
addSuperinterface(
|
||||
ClassName.bestGuess("org.jetbrains.jewel.foundation.theme.ThemeDescriptor")
|
||||
)
|
||||
|
||||
addProperty(
|
||||
PropertySpec.builder("isDark", Boolean::class, KModifier.OVERRIDE)
|
||||
.initializer("%L", themeDescriptor.dark)
|
||||
.build()
|
||||
)
|
||||
|
||||
addProperty(
|
||||
PropertySpec.builder("name", String::class, KModifier.OVERRIDE)
|
||||
.initializer("\"%L (Int UI)\"", themeDescriptor.name)
|
||||
.build()
|
||||
)
|
||||
|
||||
readColors(themeDescriptor.colors)
|
||||
readIcons(themeDescriptor)
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
|
||||
private val colorPaletteClassName = ClassName.bestGuess("org.jetbrains.jewel.foundation.theme.ThemeColorPalette")
|
||||
private val iconDataClassName = ClassName.bestGuess("org.jetbrains.jewel.foundation.theme.ThemeIconData")
|
||||
|
||||
private fun TypeSpec.Builder.readColors(colors: Map<String, String>) {
|
||||
val colorGroups =
|
||||
colors.entries
|
||||
.groupBy { it.key.replace("""\d+""".toRegex(), "") }
|
||||
.filterKeys { colorGroups.contains(it) }
|
||||
.map { (groupName, colors) ->
|
||||
// We assume color lists are in the same order as in colorGroups
|
||||
colors
|
||||
.map { (_, value) ->
|
||||
val colorHexString = value.replace("#", "0xFF")
|
||||
CodeBlock.of("Color(%L)", colorHexString)
|
||||
}
|
||||
.joinToCode(
|
||||
prefix = "\n${groupName.lowercase()} = listOf(\n",
|
||||
separator = ",\n",
|
||||
suffix = "\n)",
|
||||
)
|
||||
}
|
||||
|
||||
val rawMap =
|
||||
colors
|
||||
.map { (key, value) ->
|
||||
val colorHexString = value.replace("#", "0xFF")
|
||||
CodeBlock.of("%S to Color(%L)", key, colorHexString)
|
||||
}
|
||||
.joinToCode(prefix = "\nrawMap = mapOf(\n", separator = ",\n", suffix = "\n)")
|
||||
|
||||
addProperty(
|
||||
PropertySpec.builder("colors", colorPaletteClassName, KModifier.OVERRIDE)
|
||||
.initializer("ThemeColorPalette(%L,\n%L\n)", colorGroups.joinToCode(","), rawMap)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun TypeSpec.Builder.readIcons(theme: IntellijThemeDescriptor) {
|
||||
val iconOverrides = mutableMapOf<String, String>()
|
||||
val colorPalette = mutableMapOf<String, String>()
|
||||
|
||||
for ((key, value) in theme.icons) {
|
||||
if (value is JsonPrimitive && value.isString) {
|
||||
iconOverrides += key to value.content
|
||||
} else if (value is JsonObject && key == "ColorPalette") {
|
||||
value.entries
|
||||
.mapNotNull {
|
||||
val pairValue = it.value
|
||||
if (pairValue is JsonPrimitive && pairValue.isString) {
|
||||
it.key to pairValue.content
|
||||
} else null
|
||||
}
|
||||
.forEach { colorPalette[it.first] = it.second }
|
||||
}
|
||||
}
|
||||
|
||||
val iconOverridesBlock = iconOverrides.toMapCodeBlock()
|
||||
val selectionColorPaletteBlock = theme.iconColorsOnSelection.toMapCodeBlock()
|
||||
|
||||
addProperty(
|
||||
PropertySpec.builder("iconData", iconDataClassName, KModifier.OVERRIDE)
|
||||
.initializer(
|
||||
CodeBlock.of(
|
||||
"ThemeIconData(iconOverrides = \n%L,colorPalette = \n%L,\nselectionColorPalette = %L\n)",
|
||||
iconOverridesBlock,
|
||||
colorPalette.toMapCodeBlock(),
|
||||
selectionColorPaletteBlock,
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified K, reified V> Map<K, V>.toMapCodeBlock() =
|
||||
entries
|
||||
.map { (key, value) -> CodeBlock.of("\"%L\" to \"%L\"", key, value) }
|
||||
.joinToCode(prefix = "mapOf(", separator = ",\n", suffix = ")")
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.jetbrains.jewel.buildlogic.theme
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import java.net.URI
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.NamedDomainObjectContainer
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.kotlin.dsl.property
|
||||
|
||||
class ThemeGeneratorContainer(container: NamedDomainObjectContainer<ThemeGeneration>) :
|
||||
NamedDomainObjectContainer<ThemeGeneration> by container
|
||||
|
||||
class ThemeGeneration(val name: String, project: Project) {
|
||||
|
||||
val targetDir: DirectoryProperty =
|
||||
project.objects.directoryProperty().convention(project.layout.buildDirectory.dir("generated/theme"))
|
||||
val ideaVersion = project.objects.property<String>()
|
||||
val themeClassName = project.objects.property<String>()
|
||||
val themeFile = project.objects.property<String>()
|
||||
}
|
||||
|
||||
open class IntelliJThemeGeneratorTask : DefaultTask() {
|
||||
|
||||
@get:OutputFile val outputFile: RegularFileProperty = project.objects.fileProperty()
|
||||
|
||||
@get:Input val ideaVersion = project.objects.property<String>()
|
||||
|
||||
@get:Input val themeFile = project.objects.property<String>()
|
||||
|
||||
@get:Input val themeClassName = project.objects.property<String>()
|
||||
|
||||
init {
|
||||
group = "jewel"
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val url = buildString {
|
||||
append("https://raw.githubusercontent.com/JetBrains/intellij-community/")
|
||||
append(ideaVersion.get())
|
||||
append("/")
|
||||
append(themeFile.get())
|
||||
}
|
||||
|
||||
logger.lifecycle("Fetching theme descriptor from $url...")
|
||||
val themeDescriptor =
|
||||
URI.create(url).toURL().openStream().use { json.decodeFromStream<IntellijThemeDescriptor>(it) }
|
||||
|
||||
val className = ClassName.bestGuess(themeClassName.get())
|
||||
val file = IntUiThemeDescriptorReader.readThemeFrom(themeDescriptor, className, ideaVersion.get(), url)
|
||||
|
||||
val outputFile = outputFile.get().asFile
|
||||
logger.lifecycle(
|
||||
"Theme descriptor for ${themeDescriptor.name} parsed and " + "code generated into ${outputFile.path}"
|
||||
)
|
||||
outputFile.bufferedWriter().use { file.writeTo(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class IntellijThemeDescriptor(
|
||||
val name: String,
|
||||
val author: String = "",
|
||||
val dark: Boolean = false,
|
||||
val editorScheme: String,
|
||||
val colors: Map<String, String> = emptyMap(),
|
||||
val ui: Map<String, JsonElement> = emptyMap(),
|
||||
val icons: Map<String, JsonElement> = emptyMap(),
|
||||
val iconColorsOnSelection: Map<String, Int> = emptyMap(),
|
||||
)
|
||||
253
platform/jewel/decorated-window/api/decorated-window.api
Normal file
253
platform/jewel/decorated-window/api/decorated-window.api
Normal file
@@ -0,0 +1,253 @@
|
||||
public abstract interface class com/jetbrains/DesktopActions {
|
||||
public abstract fun setHandler (Lcom/jetbrains/DesktopActions$Handler;)V
|
||||
}
|
||||
|
||||
public abstract interface class com/jetbrains/DesktopActions$Handler {
|
||||
public fun browse (Ljava/net/URI;)V
|
||||
public fun edit (Ljava/io/File;)V
|
||||
public fun mail (Ljava/net/URI;)V
|
||||
public fun open (Ljava/io/File;)V
|
||||
public fun print (Ljava/io/File;)V
|
||||
}
|
||||
|
||||
public class com/jetbrains/JBR {
|
||||
public static fun getApiVersion ()Ljava/lang/String;
|
||||
public static fun getDesktopActions ()Lcom/jetbrains/DesktopActions;
|
||||
public static fun getRoundedCornersManager ()Lcom/jetbrains/RoundedCornersManager;
|
||||
public static fun getWindowDecorations ()Lcom/jetbrains/WindowDecorations;
|
||||
public static fun getWindowMove ()Lcom/jetbrains/WindowMove;
|
||||
public static fun isAvailable ()Z
|
||||
public static fun isDesktopActionsSupported ()Z
|
||||
public static fun isRoundedCornersManagerSupported ()Z
|
||||
public static fun isWindowDecorationsSupported ()Z
|
||||
public static fun isWindowMoveSupported ()Z
|
||||
}
|
||||
|
||||
public abstract interface class com/jetbrains/RoundedCornersManager {
|
||||
public abstract fun setRoundedCorners (Ljava/awt/Window;Ljava/lang/Object;)V
|
||||
}
|
||||
|
||||
public abstract interface class com/jetbrains/WindowDecorations {
|
||||
public abstract fun createCustomTitleBar ()Lcom/jetbrains/WindowDecorations$CustomTitleBar;
|
||||
public abstract fun setCustomTitleBar (Ljava/awt/Dialog;Lcom/jetbrains/WindowDecorations$CustomTitleBar;)V
|
||||
public abstract fun setCustomTitleBar (Ljava/awt/Frame;Lcom/jetbrains/WindowDecorations$CustomTitleBar;)V
|
||||
}
|
||||
|
||||
public abstract interface class com/jetbrains/WindowDecorations$CustomTitleBar {
|
||||
public abstract fun forceHitTest (Z)V
|
||||
public abstract fun getContainingWindow ()Ljava/awt/Window;
|
||||
public abstract fun getHeight ()F
|
||||
public abstract fun getLeftInset ()F
|
||||
public abstract fun getProperties ()Ljava/util/Map;
|
||||
public abstract fun getRightInset ()F
|
||||
public abstract fun putProperties (Ljava/util/Map;)V
|
||||
public abstract fun putProperty (Ljava/lang/String;Ljava/lang/Object;)V
|
||||
public abstract fun setHeight (F)V
|
||||
}
|
||||
|
||||
public abstract interface class com/jetbrains/WindowMove {
|
||||
public abstract fun startMovingTogetherWithMouse (Ljava/awt/Window;I)V
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/DecoratedWindowKt {
|
||||
public static final fun DecoratedWindow (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/jewel/window/styling/DecoratedWindowStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
|
||||
}
|
||||
|
||||
public abstract interface class org/jetbrains/jewel/window/DecoratedWindowScope : androidx/compose/ui/window/FrameWindowScope {
|
||||
public abstract fun getState-VA8cQZQ ()J
|
||||
public abstract fun getWindow ()Landroidx/compose/ui/awt/ComposeWindow;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/DecoratedWindowState {
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/DecoratedWindowState$Companion;
|
||||
public static final synthetic fun box-impl (J)Lorg/jetbrains/jewel/window/DecoratedWindowState;
|
||||
public static fun constructor-impl (J)J
|
||||
public static final fun copy-zAQEbgo (JZZZZ)J
|
||||
public static synthetic fun copy-zAQEbgo$default (JZZZZILjava/lang/Object;)J
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public static fun equals-impl (JLjava/lang/Object;)Z
|
||||
public static final fun equals-impl0 (JJ)Z
|
||||
public final fun getState-s-VKNKU ()J
|
||||
public fun hashCode ()I
|
||||
public static fun hashCode-impl (J)I
|
||||
public static final fun isActive-impl (J)Z
|
||||
public static final fun isFullscreen-impl (J)Z
|
||||
public static final fun isMaximized-impl (J)Z
|
||||
public static final fun isMinimized-impl (J)Z
|
||||
public fun toString ()Ljava/lang/String;
|
||||
public static fun toString-impl (J)Ljava/lang/String;
|
||||
public final synthetic fun unbox-impl ()J
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/DecoratedWindowState$Companion {
|
||||
public final fun getActive-s-VKNKU ()J
|
||||
public final fun getFullscreen-s-VKNKU ()J
|
||||
public final fun getMaximize-s-VKNKU ()J
|
||||
public final fun getMinimize-s-VKNKU ()J
|
||||
public final fun of-LPCgXDc (Landroidx/compose/ui/awt/ComposeWindow;)J
|
||||
public final fun of-zAQEbgo (ZZZZ)J
|
||||
public static synthetic fun of-zAQEbgo$default (Lorg/jetbrains/jewel/window/DecoratedWindowState$Companion;ZZZZILjava/lang/Object;)J
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/ThemeKt {
|
||||
public static final fun getDefaultDecoratedWindowStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/window/styling/DecoratedWindowStyle;
|
||||
public static final fun getDefaultTitleBarStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/window/styling/TitleBarStyle;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/TitleBarKt {
|
||||
public static final fun TitleBar-T042LqI (Lorg/jetbrains/jewel/window/DecoratedWindowScope;Landroidx/compose/ui/Modifier;JLorg/jetbrains/jewel/window/styling/TitleBarStyle;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public abstract interface class org/jetbrains/jewel/window/TitleBarScope {
|
||||
public abstract fun align (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment$Horizontal;)Landroidx/compose/ui/Modifier;
|
||||
public abstract fun getIcon ()Landroidx/compose/ui/graphics/painter/Painter;
|
||||
public abstract fun getTitle ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/TitleBar_MacOSKt {
|
||||
public static final fun newFullscreenControls (Landroidx/compose/ui/Modifier;Z)Landroidx/compose/ui/Modifier;
|
||||
public static synthetic fun newFullscreenControls$default (Landroidx/compose/ui/Modifier;ZILjava/lang/Object;)Landroidx/compose/ui/Modifier;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/DecoratedWindowColors {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/styling/DecoratedWindowColors$Companion;
|
||||
public synthetic fun <init> (JJLkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun borderFor-3hEOMOc (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getBorder-0d7_KjU ()J
|
||||
public final fun getBorderInactive-0d7_KjU ()J
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/DecoratedWindowColors$Companion {
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/DecoratedWindowMetrics {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/styling/DecoratedWindowMetrics$Companion;
|
||||
public synthetic fun <init> (FLkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getBorderWidth-D9Ej5fM ()F
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/DecoratedWindowMetrics$Companion {
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/DecoratedWindowStyle {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/styling/DecoratedWindowStyle$Companion;
|
||||
public fun <init> (Lorg/jetbrains/jewel/window/styling/DecoratedWindowColors;Lorg/jetbrains/jewel/window/styling/DecoratedWindowMetrics;)V
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getColors ()Lorg/jetbrains/jewel/window/styling/DecoratedWindowColors;
|
||||
public final fun getMetrics ()Lorg/jetbrains/jewel/window/styling/DecoratedWindowMetrics;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/DecoratedWindowStyle$Companion {
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/DecoratedWindowStylingKt {
|
||||
public static final fun getLocalDecoratedWindowStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarColors {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarColors$Companion;
|
||||
public synthetic fun <init> (JJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun backgroundFor-3hEOMOc (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getBackground-0d7_KjU ()J
|
||||
public final fun getBorder-0d7_KjU ()J
|
||||
public final fun getContent-0d7_KjU ()J
|
||||
public final fun getDropdownHoveredBackground-0d7_KjU ()J
|
||||
public final fun getDropdownPressedBackground-0d7_KjU ()J
|
||||
public final fun getFullscreenControlButtonsBackground-0d7_KjU ()J
|
||||
public final fun getIconButtonHoveredBackground-0d7_KjU ()J
|
||||
public final fun getIconButtonPressedBackground-0d7_KjU ()J
|
||||
public final fun getInactiveBackground-0d7_KjU ()J
|
||||
public final fun getTitlePaneButtonHoveredBackground-0d7_KjU ()J
|
||||
public final fun getTitlePaneButtonPressedBackground-0d7_KjU ()J
|
||||
public final fun getTitlePaneCloseButtonHoveredBackground-0d7_KjU ()J
|
||||
public final fun getTitlePaneCloseButtonPressedBackground-0d7_KjU ()J
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarColors$Companion {
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarIcons {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarIcons$Companion;
|
||||
public fun <init> (Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;)V
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getCloseButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
|
||||
public final fun getMaximizeButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
|
||||
public final fun getMinimizeButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
|
||||
public final fun getRestoreButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarIcons$Companion {
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarMetrics {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarMetrics$Companion;
|
||||
public synthetic fun <init> (FFFJLkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getGradientEndX-D9Ej5fM ()F
|
||||
public final fun getGradientStartX-D9Ej5fM ()F
|
||||
public final fun getHeight-D9Ej5fM ()F
|
||||
public final fun getTitlePaneButtonSize-MYxV2XQ ()J
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarMetrics$Companion {
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarStyle {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarStyle$Companion;
|
||||
public fun <init> (Lorg/jetbrains/jewel/window/styling/TitleBarColors;Lorg/jetbrains/jewel/window/styling/TitleBarMetrics;Lorg/jetbrains/jewel/window/styling/TitleBarIcons;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;)V
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getColors ()Lorg/jetbrains/jewel/window/styling/TitleBarColors;
|
||||
public final fun getDropdownStyle ()Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;
|
||||
public final fun getIconButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;
|
||||
public final fun getIcons ()Lorg/jetbrains/jewel/window/styling/TitleBarIcons;
|
||||
public final fun getMetrics ()Lorg/jetbrains/jewel/window/styling/TitleBarMetrics;
|
||||
public final fun getPaneButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;
|
||||
public final fun getPaneCloseButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarStyle$Companion {
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/styling/TitleBarStylingKt {
|
||||
public static final fun getLocalTitleBarStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/utils/DesktopPlatform : java/lang/Enum {
|
||||
public static final field Companion Lorg/jetbrains/jewel/window/utils/DesktopPlatform$Companion;
|
||||
public static final field Linux Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
|
||||
public static final field MacOS Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
|
||||
public static final field Unknown Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
|
||||
public static final field Windows Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
|
||||
public static fun getEntries ()Lkotlin/enums/EnumEntries;
|
||||
public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
|
||||
public static fun values ()[Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
|
||||
}
|
||||
|
||||
public final class org/jetbrains/jewel/window/utils/DesktopPlatform$Companion {
|
||||
public final fun getCurrent ()Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
|
||||
}
|
||||
|
||||
18
platform/jewel/decorated-window/build.gradle.kts
Normal file
18
platform/jewel/decorated-window/build.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
||||
import org.jetbrains.compose.ComposeBuildConfig
|
||||
|
||||
plugins {
|
||||
jewel
|
||||
`jewel-publish`
|
||||
`jewel-check-public-api`
|
||||
alias(libs.plugins.composeDesktop)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
private val composeVersion
|
||||
get() = ComposeBuildConfig.composeVersion
|
||||
|
||||
dependencies {
|
||||
api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion")
|
||||
api(projects.ui)
|
||||
implementation(libs.jna.core)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2000-2022 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.jetbrains;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
public interface DesktopActions {
|
||||
|
||||
void setHandler(Handler handler);
|
||||
|
||||
interface Handler {
|
||||
default void open(File file) throws IOException { throw new UnsupportedOperationException(); }
|
||||
default void edit(File file) throws IOException { throw new UnsupportedOperationException(); }
|
||||
default void print(File file) throws IOException { throw new UnsupportedOperationException(); }
|
||||
default void mail(URI mailtoURL) throws IOException { throw new UnsupportedOperationException(); }
|
||||
default void browse(URI uri) throws IOException { throw new UnsupportedOperationException(); }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2000-2023 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.jetbrains;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
/**
|
||||
* This class is an entry point into JBR API.
|
||||
* JBR API is a collection of services, classes, interfaces, etc.,
|
||||
* which require tight interaction with JRE and therefore are implemented inside JBR.
|
||||
* <div>JBR API consists of two parts:</div>
|
||||
* <ul>
|
||||
* <li>Client side - {@code jetbrains.api} module, mostly containing interfaces</li>
|
||||
* <li>JBR side - actual implementation code inside JBR</li>
|
||||
* </ul>
|
||||
* Client and JBR side are linked dynamically at runtime and do not have to be of the same version.
|
||||
* In some cases (e.g. running on different JRE or old JBR) system will not be able to find
|
||||
* implementation for some services, so you'll need a fallback behavior for that case.
|
||||
* <h2>Simple usage example:</h2>
|
||||
* <blockquote><pre>{@code
|
||||
* if (JBR.isSomeServiceSupported()) {
|
||||
* JBR.getSomeService().doSomething();
|
||||
* } else {
|
||||
* planB();
|
||||
* }
|
||||
* }</pre></blockquote>
|
||||
*
|
||||
* @implNote JBR API is initialized on first access to this class (in static initializer).
|
||||
* Actual implementation is linked on demand, when corresponding service is requested by client.
|
||||
*/
|
||||
public class JBR {
|
||||
|
||||
private static final ServiceApi api;
|
||||
private static final Exception bootstrapException;
|
||||
|
||||
static {
|
||||
ServiceApi a = null;
|
||||
Exception exception = null;
|
||||
try {
|
||||
a = (ServiceApi) Class.forName("com.jetbrains.bootstrap.JBRApiBootstrap")
|
||||
.getMethod("bootstrap", MethodHandles.Lookup.class)
|
||||
.invoke(null, MethodHandles.lookup());
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable t = e.getCause();
|
||||
if (t instanceof Error error) throw error;
|
||||
else throw new Error(t);
|
||||
} catch (IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {
|
||||
exception = e;
|
||||
}
|
||||
api = a;
|
||||
bootstrapException = exception;
|
||||
}
|
||||
|
||||
private JBR() {
|
||||
}
|
||||
|
||||
private static <T> T getService(Class<T> interFace, FallbackSupplier<T> fallback) {
|
||||
T service = getService(interFace);
|
||||
try {
|
||||
return service != null ? service : fallback != null ? fallback.get() : null;
|
||||
} catch (Throwable ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T getService(Class<T> interFace) {
|
||||
return api == null ? null : api.getService(interFace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true when running on JBR which implements JBR API
|
||||
*/
|
||||
public static boolean isAvailable() {
|
||||
return api != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JBR API version in form {@code JBR.MAJOR.MINOR.PATCH}
|
||||
* @implNote This is an API version, which comes with client application,
|
||||
* it has nothing to do with JRE it runs on.
|
||||
*/
|
||||
public static String getApiVersion() {
|
||||
return "17.0.8.1b1070.2.1.9.0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal API interface, contains most basic methods for communication between client and JBR.
|
||||
*/
|
||||
private interface ServiceApi {
|
||||
|
||||
<T> T getService(Class<T> interFace);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface FallbackSupplier<T> {
|
||||
T get() throws Throwable;
|
||||
}
|
||||
|
||||
// ========================== Generated metadata ==========================
|
||||
|
||||
/**
|
||||
* Generated client-side metadata, needed by JBR when linking the implementation.
|
||||
*/
|
||||
private static final class Metadata {
|
||||
private static final String[] KNOWN_SERVICES = {"com.jetbrains.ExtendedGlyphCache", "com.jetbrains.DesktopActions", "com.jetbrains.CustomWindowDecoration", "com.jetbrains.ProjectorUtils", "com.jetbrains.FontExtensions", "com.jetbrains.RoundedCornersManager", "com.jetbrains.GraphicsUtils", "com.jetbrains.WindowDecorations", "com.jetbrains.JBRFileDialogService", "com.jetbrains.AccessibleAnnouncer", "com.jetbrains.JBR$ServiceApi", "com.jetbrains.Jstack", "com.jetbrains.WindowMove"};
|
||||
private static final String[] KNOWN_PROXIES = {"com.jetbrains.JBRFileDialog", "com.jetbrains.WindowDecorations$CustomTitleBar"};
|
||||
}
|
||||
|
||||
// ======================= Generated static methods =======================
|
||||
|
||||
private static class DesktopActions__Holder {
|
||||
private static final DesktopActions INSTANCE = getService(DesktopActions.class, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if current runtime has implementation for all methods in {@link DesktopActions}
|
||||
* and its dependencies (can fully implement given service).
|
||||
* @see #getDesktopActions()
|
||||
*/
|
||||
public static boolean isDesktopActionsSupported() {
|
||||
return DesktopActions__Holder.INSTANCE != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return full implementation of {@link DesktopActions} service if any, or {@code null} otherwise
|
||||
*/
|
||||
public static DesktopActions getDesktopActions() {
|
||||
return DesktopActions__Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private static class RoundedCornersManager__Holder {
|
||||
private static final RoundedCornersManager INSTANCE = getService(RoundedCornersManager.class, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if current runtime has implementation for all methods in {@link RoundedCornersManager}
|
||||
* and its dependencies (can fully implement given service).
|
||||
* @see #getRoundedCornersManager()
|
||||
*/
|
||||
public static boolean isRoundedCornersManagerSupported() {
|
||||
return RoundedCornersManager__Holder.INSTANCE != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This manager allows decorate awt Window with rounded corners.
|
||||
* Appearance depends from operating system.
|
||||
*
|
||||
* @return full implementation of {@link RoundedCornersManager} service if any, or {@code null} otherwise
|
||||
*/
|
||||
public static RoundedCornersManager getRoundedCornersManager() {
|
||||
return RoundedCornersManager__Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private static class WindowDecorations__Holder {
|
||||
private static final WindowDecorations INSTANCE = getService(WindowDecorations.class, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if current runtime has implementation for all methods in {@link WindowDecorations}
|
||||
* and its dependencies (can fully implement given service).
|
||||
* @see #getWindowDecorations()
|
||||
*/
|
||||
public static boolean isWindowDecorationsSupported() {
|
||||
return WindowDecorations__Holder.INSTANCE != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Window decorations consist of title bar, window controls and border.
|
||||
*
|
||||
* @return full implementation of {@link WindowDecorations} service if any, or {@code null} otherwise
|
||||
* @see WindowDecorations.CustomTitleBar
|
||||
*/
|
||||
public static WindowDecorations getWindowDecorations() {
|
||||
return WindowDecorations__Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private static class WindowMove__Holder {
|
||||
private static final WindowMove INSTANCE = getService(WindowMove.class, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if current runtime has implementation for all methods in {@link WindowMove}
|
||||
* and its dependencies (can fully implement given service).
|
||||
* @see #getWindowMove()
|
||||
*/
|
||||
public static boolean isWindowMoveSupported() {
|
||||
return WindowMove__Holder.INSTANCE != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return full implementation of {@link WindowMove} service if any, or {@code null} otherwise
|
||||
*/
|
||||
public static WindowMove getWindowMove() {
|
||||
return WindowMove__Holder.INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2000-2023 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.jetbrains;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* This manager allows decorate awt Window with rounded corners.
|
||||
* Appearance depends from operating system.
|
||||
*/
|
||||
public interface RoundedCornersManager {
|
||||
/**
|
||||
* @param params for macOS is Float object with radius or
|
||||
* Array with {Float for radius, Integer for border width, java.awt.Color for border color}.
|
||||
*
|
||||
* @param params for Windows 11 is String with values:
|
||||
* "default" - let the system decide whether or not to round window corners,
|
||||
* "none" - never round window corners,
|
||||
* "full" - round the corners if appropriate,
|
||||
* "small" - round the corners if appropriate, with a small radius.
|
||||
*/
|
||||
void setRoundedCorners(Window window, Object params);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2023 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.jetbrains;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Window decorations consist of title bar, window controls and border.
|
||||
* @see CustomTitleBar
|
||||
*/
|
||||
public interface WindowDecorations {
|
||||
|
||||
/**
|
||||
* If {@code customTitleBar} is not null, system-provided title bar is removed and client area is extended to the
|
||||
* top of the frame with window controls painted over the client area.
|
||||
* {@code customTitleBar=null} resets to the default appearance with system-provided title bar.
|
||||
* @see CustomTitleBar
|
||||
* @see #createCustomTitleBar()
|
||||
*/
|
||||
void setCustomTitleBar(Frame frame, CustomTitleBar customTitleBar);
|
||||
|
||||
/**
|
||||
* If {@code customTitleBar} is not null, system-provided title bar is removed and client area is extended to the
|
||||
* top of the dialog with window controls painted over the client area.
|
||||
* {@code customTitleBar=null} resets to the default appearance with system-provided title bar.
|
||||
* @see CustomTitleBar
|
||||
* @see #createCustomTitleBar()
|
||||
*/
|
||||
void setCustomTitleBar(Dialog dialog, CustomTitleBar customTitleBar);
|
||||
|
||||
/**
|
||||
* You must {@linkplain CustomTitleBar#setHeight(float) set title bar height} before adding it to a window.
|
||||
* @see CustomTitleBar
|
||||
* @see #setCustomTitleBar(Frame, CustomTitleBar)
|
||||
* @see #setCustomTitleBar(Dialog, CustomTitleBar)
|
||||
*/
|
||||
CustomTitleBar createCustomTitleBar();
|
||||
|
||||
/**
|
||||
* Custom title bar allows merging of window content with native title bar,
|
||||
* which is done by treating title bar as part of client area, but with some
|
||||
* special behavior like dragging or maximizing on double click.
|
||||
* Custom title bar has {@linkplain CustomTitleBar#getHeight() height} and controls.
|
||||
* @implNote Behavior is platform-dependent, only macOS and Windows are supported.
|
||||
* @see #setCustomTitleBar(Frame, CustomTitleBar)
|
||||
*/
|
||||
interface CustomTitleBar {
|
||||
|
||||
/**
|
||||
* @return title bar height, measured in pixels from the top of client area, i.e. excluding top frame border.
|
||||
*/
|
||||
float getHeight();
|
||||
|
||||
/**
|
||||
* @param height title bar height, measured in pixels from the top of client area,
|
||||
* i.e. excluding top frame border. Must be > 0.
|
||||
*/
|
||||
void setHeight(float height);
|
||||
|
||||
/**
|
||||
* @see #putProperty(String, Object)
|
||||
*/
|
||||
Map<String, Object> getProperties();
|
||||
|
||||
/**
|
||||
* @see #putProperty(String, Object)
|
||||
*/
|
||||
void putProperties(Map<String, ?> m);
|
||||
|
||||
/**
|
||||
* Windows & macOS properties:
|
||||
* <ul>
|
||||
* <li>{@code controls.visible} : {@link Boolean} - whether title bar controls
|
||||
* (minimize/maximize/close buttons) are visible, default = true.</li>
|
||||
* </ul>
|
||||
* Windows properties:
|
||||
* <ul>
|
||||
* <li>{@code controls.width} : {@link Number} - width of block of buttons (not individual buttons).
|
||||
* Note that dialogs have only one button, while frames usually have 3 of them.</li>
|
||||
* <li>{@code controls.dark} : {@link Boolean} - whether to use dark or light color theme
|
||||
* (light or dark icons respectively).</li>
|
||||
* <li>{@code controls.<layer>.<state>} : {@link Color} - precise control over button colors,
|
||||
* where {@code <layer>} is one of:
|
||||
* <ul><li>{@code foreground}</li><li>{@code background}</li></ul>
|
||||
* and {@code <state>} is one of:
|
||||
* <ul>
|
||||
* <li>{@code normal}</li>
|
||||
* <li>{@code hovered}</li>
|
||||
* <li>{@code pressed}</li>
|
||||
* <li>{@code disabled}</li>
|
||||
* <li>{@code inactive}</li>
|
||||
* </ul>
|
||||
* </ul>
|
||||
*/
|
||||
void putProperty(String key, Object value);
|
||||
|
||||
/**
|
||||
* @return space occupied by title bar controls on the left (px)
|
||||
*/
|
||||
float getLeftInset();
|
||||
/**
|
||||
* @return space occupied by title bar controls on the right (px)
|
||||
*/
|
||||
float getRightInset();
|
||||
|
||||
/**
|
||||
* By default, any component which has no cursor or mouse event listeners set is considered transparent for
|
||||
* native title bar actions. That is, dragging simple JPanel in title bar area will drag the
|
||||
* window, but dragging a JButton will not. Adding mouse listener to a component will prevent any native actions
|
||||
* inside bounds of that component.
|
||||
* <p>
|
||||
* This method gives you precise control of whether to allow native title bar actions or not.
|
||||
* <ul>
|
||||
* <li>{@code client=true} means that mouse is currently over a client area. Native title bar behavior is disabled.</li>
|
||||
* <li>{@code client=false} means that mouse is currently over a non-client area. Native title bar behavior is enabled.</li>
|
||||
* </ul>
|
||||
* <em>Intended usage:
|
||||
* <ul>
|
||||
* <li>This method must be called in response to all {@linkplain java.awt.event.MouseEvent mouse events}
|
||||
* except {@link java.awt.event.MouseEvent#MOUSE_EXITED} and {@link java.awt.event.MouseEvent#MOUSE_WHEEL}.</li>
|
||||
* <li>This method is called per-event, i.e. when component has multiple listeners, you only need to call it once.</li>
|
||||
* <li>If this method hadn't been called, title bar behavior is reverted back to default upon processing the event.</li>
|
||||
* </ul></em>
|
||||
* Note that hit test value is relevant only for title bar area, e.g. calling
|
||||
* {@code forceHitTest(false)} will not make window draggable via non-title bar area.
|
||||
*
|
||||
* <h2>Example:</h2>
|
||||
* Suppose you have a {@code JPanel} in the title bar area. You want it to respond to right-click for
|
||||
* some popup menu, but also retain native drag and double-click behavior.
|
||||
* <pre>
|
||||
* CustomTitleBar titlebar = ...;
|
||||
* JPanel panel = ...;
|
||||
* MouseAdapter adapter = new MouseAdapter() {
|
||||
* private void hit() { titlebar.forceHitTest(false); }
|
||||
* public void mouseClicked(MouseEvent e) {
|
||||
* hit();
|
||||
* if (e.getButton() == MouseEvent.BUTTON3) ...;
|
||||
* }
|
||||
* public void mousePressed(MouseEvent e) { hit(); }
|
||||
* public void mouseReleased(MouseEvent e) { hit(); }
|
||||
* public void mouseEntered(MouseEvent e) { hit(); }
|
||||
* public void mouseDragged(MouseEvent e) { hit(); }
|
||||
* public void mouseMoved(MouseEvent e) { hit(); }
|
||||
* };
|
||||
* panel.addMouseListener(adapter);
|
||||
* panel.addMouseMotionListener(adapter);
|
||||
* </pre>
|
||||
*/
|
||||
void forceHitTest(boolean client);
|
||||
|
||||
Window getContainingWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2000-2023 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.jetbrains;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public interface WindowMove {
|
||||
/**
|
||||
* Starts moving the top-level parent window of the given window together with the mouse pointer.
|
||||
* The intended use is to facilitate the implementation of window management similar to the way
|
||||
* it is done natively on the platform.
|
||||
*
|
||||
* Preconditions for calling this method:
|
||||
* <ul>
|
||||
* <li>WM supports _NET_WM_MOVE_RESIZE (this is checked automatically when an implementation
|
||||
* of this interface is obtained).</li>
|
||||
* <li>Mouse pointer is within this window's bounds.</li>
|
||||
* <li>The mouse button specified by {@code mouseButton} is pressed.</li>
|
||||
* </ul>
|
||||
*
|
||||
* Calling this method will make the window start moving together with the mouse pointer until
|
||||
* the specified mouse button is released or Esc is pressed. The conditions for cancelling
|
||||
* the move may differ between WMs.
|
||||
*
|
||||
* @param mouseButton indicates the mouse button that was pressed to start moving the window;
|
||||
* must be one of {@code MouseEvent.BUTTON1}, {@code MouseEvent.BUTTON2},
|
||||
* or {@code MouseEvent.BUTTON3}.
|
||||
*/
|
||||
void startMovingTogetherWithMouse(Window window, int mouseButton);
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package org.jetbrains.jewel.window
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.awt.ComposeWindow
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.MeasurePolicy
|
||||
import androidx.compose.ui.layout.MeasureResult
|
||||
import androidx.compose.ui.layout.MeasureScope
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.offset
|
||||
import androidx.compose.ui.window.FrameWindowScope
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowPlacement
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import com.jetbrains.JBR
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.event.ComponentListener
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import org.jetbrains.jewel.foundation.Stroke
|
||||
import org.jetbrains.jewel.foundation.modifier.border
|
||||
import org.jetbrains.jewel.foundation.modifier.trackWindowActivation
|
||||
import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.window.styling.DecoratedWindowStyle
|
||||
import org.jetbrains.jewel.window.utils.DesktopPlatform
|
||||
|
||||
@Composable
|
||||
public fun DecoratedWindow(
|
||||
onCloseRequest: () -> Unit,
|
||||
state: WindowState = rememberWindowState(),
|
||||
visible: Boolean = true,
|
||||
title: String = "",
|
||||
icon: Painter? = null,
|
||||
resizable: Boolean = true,
|
||||
enabled: Boolean = true,
|
||||
focusable: Boolean = true,
|
||||
alwaysOnTop: Boolean = false,
|
||||
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||
onKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||
style: DecoratedWindowStyle = JewelTheme.defaultDecoratedWindowStyle,
|
||||
content: @Composable DecoratedWindowScope.() -> Unit,
|
||||
) {
|
||||
remember {
|
||||
if (!JBR.isAvailable()) {
|
||||
error(
|
||||
"DecoratedWindow can only be used on JetBrainsRuntime(JBR) platform, " +
|
||||
"please check the document https://github.com/JetBrains/jewel#int-ui-standalone-theme"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Using undecorated window for linux
|
||||
val undecorated = DesktopPlatform.Linux == DesktopPlatform.Current
|
||||
|
||||
Window(
|
||||
onCloseRequest,
|
||||
state,
|
||||
visible,
|
||||
title,
|
||||
icon,
|
||||
undecorated,
|
||||
transparent = false,
|
||||
resizable,
|
||||
enabled,
|
||||
focusable,
|
||||
alwaysOnTop,
|
||||
onPreviewKeyEvent,
|
||||
onKeyEvent,
|
||||
) {
|
||||
var decoratedWindowState by remember { mutableStateOf(DecoratedWindowState.of(window)) }
|
||||
|
||||
DisposableEffect(window) {
|
||||
val adapter =
|
||||
object : WindowAdapter(), ComponentListener {
|
||||
override fun windowActivated(e: WindowEvent?) {
|
||||
decoratedWindowState = DecoratedWindowState.of(window)
|
||||
}
|
||||
|
||||
override fun windowDeactivated(e: WindowEvent?) {
|
||||
decoratedWindowState = DecoratedWindowState.of(window)
|
||||
}
|
||||
|
||||
override fun windowIconified(e: WindowEvent?) {
|
||||
decoratedWindowState = DecoratedWindowState.of(window)
|
||||
}
|
||||
|
||||
override fun windowDeiconified(e: WindowEvent?) {
|
||||
decoratedWindowState = DecoratedWindowState.of(window)
|
||||
}
|
||||
|
||||
override fun windowStateChanged(e: WindowEvent) {
|
||||
decoratedWindowState = DecoratedWindowState.of(window)
|
||||
}
|
||||
|
||||
override fun componentResized(e: ComponentEvent?) {
|
||||
decoratedWindowState = DecoratedWindowState.of(window)
|
||||
}
|
||||
|
||||
override fun componentMoved(e: ComponentEvent?) {
|
||||
// Empty
|
||||
}
|
||||
|
||||
override fun componentShown(e: ComponentEvent?) {
|
||||
// Empty
|
||||
}
|
||||
|
||||
override fun componentHidden(e: ComponentEvent?) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
|
||||
window.addWindowListener(adapter)
|
||||
window.addWindowStateListener(adapter)
|
||||
window.addComponentListener(adapter)
|
||||
|
||||
onDispose {
|
||||
window.removeWindowListener(adapter)
|
||||
window.removeWindowStateListener(adapter)
|
||||
window.removeComponentListener(adapter)
|
||||
}
|
||||
}
|
||||
|
||||
val undecoratedWindowBorder =
|
||||
if (undecorated && !decoratedWindowState.isMaximized) {
|
||||
Modifier.border(
|
||||
Stroke.Alignment.Inside,
|
||||
style.metrics.borderWidth,
|
||||
style.colors.borderFor(decoratedWindowState).value,
|
||||
RectangleShape,
|
||||
)
|
||||
.padding(style.metrics.borderWidth)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTitleBarInfo provides TitleBarInfo(title, icon)) {
|
||||
Layout(
|
||||
content = {
|
||||
val scope =
|
||||
object : DecoratedWindowScope {
|
||||
override val state: DecoratedWindowState
|
||||
get() = decoratedWindowState
|
||||
|
||||
override val window: ComposeWindow
|
||||
get() = this@Window.window
|
||||
}
|
||||
scope.content()
|
||||
},
|
||||
modifier = undecoratedWindowBorder.trackWindowActivation(window),
|
||||
measurePolicy = DecoratedWindowMeasurePolicy,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
public interface DecoratedWindowScope : FrameWindowScope {
|
||||
override val window: ComposeWindow
|
||||
|
||||
public val state: DecoratedWindowState
|
||||
}
|
||||
|
||||
private object DecoratedWindowMeasurePolicy : MeasurePolicy {
|
||||
override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
|
||||
if (measurables.isEmpty()) {
|
||||
return layout(width = constraints.minWidth, height = constraints.minHeight) {}
|
||||
}
|
||||
|
||||
val titleBars = measurables.filter { it.layoutId == TITLE_BAR_LAYOUT_ID }
|
||||
if (titleBars.size > 1) {
|
||||
error("Window just can have only one title bar")
|
||||
}
|
||||
val titleBar = titleBars.firstOrNull()
|
||||
val titleBarBorder = measurables.firstOrNull { it.layoutId == TITLE_BAR_BORDER_LAYOUT_ID }
|
||||
|
||||
val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||
|
||||
val titleBarPlaceable = titleBar?.measure(contentConstraints)
|
||||
val titleBarHeight = titleBarPlaceable?.height ?: 0
|
||||
|
||||
val titleBarBorderPlaceable = titleBarBorder?.measure(contentConstraints)
|
||||
val titleBarBorderHeight = titleBarBorderPlaceable?.height ?: 0
|
||||
|
||||
val measuredPlaceable = mutableListOf<Placeable>()
|
||||
|
||||
for (it in measurables) {
|
||||
if (it.layoutId.toString().startsWith(TITLE_BAR_COMPONENT_LAYOUT_ID_PREFIX)) continue
|
||||
val offsetConstraints = contentConstraints.offset(vertical = -titleBarHeight - titleBarBorderHeight)
|
||||
val placeable = it.measure(offsetConstraints)
|
||||
measuredPlaceable += placeable
|
||||
}
|
||||
|
||||
return layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
titleBarPlaceable?.placeRelative(0, 0)
|
||||
titleBarBorderPlaceable?.placeRelative(0, titleBarHeight)
|
||||
|
||||
measuredPlaceable.forEach { it.placeRelative(0, titleBarHeight + titleBarBorderHeight) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@JvmInline
|
||||
public value class DecoratedWindowState(public val state: ULong) {
|
||||
public val isActive: Boolean
|
||||
get() = state and Active != 0UL
|
||||
|
||||
public val isFullscreen: Boolean
|
||||
get() = state and Fullscreen != 0UL
|
||||
|
||||
public val isMinimized: Boolean
|
||||
get() = state and Minimize != 0UL
|
||||
|
||||
public val isMaximized: Boolean
|
||||
get() = state and Maximize != 0UL
|
||||
|
||||
public fun copy(
|
||||
fullscreen: Boolean = isFullscreen,
|
||||
minimized: Boolean = isMinimized,
|
||||
maximized: Boolean = isMaximized,
|
||||
active: Boolean = isActive,
|
||||
): DecoratedWindowState = of(fullscreen = fullscreen, minimized = minimized, maximized = maximized, active = active)
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}(isFullscreen=$isFullscreen, isActive=$isActive)"
|
||||
|
||||
public companion object {
|
||||
public val Active: ULong = 1UL shl 0
|
||||
public val Fullscreen: ULong = 1UL shl 1
|
||||
public val Minimize: ULong = 1UL shl 2
|
||||
public val Maximize: ULong = 1UL shl 3
|
||||
|
||||
public fun of(
|
||||
fullscreen: Boolean = false,
|
||||
minimized: Boolean = false,
|
||||
maximized: Boolean = false,
|
||||
active: Boolean = true,
|
||||
): DecoratedWindowState =
|
||||
DecoratedWindowState(
|
||||
(if (fullscreen) Fullscreen else 0UL) or
|
||||
(if (minimized) Minimize else 0UL) or
|
||||
(if (maximized) Maximize else 0UL) or
|
||||
(if (active) Active else 0UL)
|
||||
)
|
||||
|
||||
public fun of(window: ComposeWindow): DecoratedWindowState =
|
||||
of(
|
||||
fullscreen = window.placement == WindowPlacement.Fullscreen,
|
||||
minimized = window.isMinimized,
|
||||
maximized = window.placement == WindowPlacement.Maximized,
|
||||
active = window.isActive,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class TitleBarInfo(val title: String, val icon: Painter?)
|
||||
|
||||
internal val LocalTitleBarInfo: ProvidableCompositionLocal<TitleBarInfo> = compositionLocalOf {
|
||||
error("LocalTitleBarInfo not provided, TitleBar must be used in DecoratedWindow")
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jetbrains.jewel.window
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.window.styling.DecoratedWindowStyle
|
||||
import org.jetbrains.jewel.window.styling.LocalDecoratedWindowStyle
|
||||
import org.jetbrains.jewel.window.styling.LocalTitleBarStyle
|
||||
import org.jetbrains.jewel.window.styling.TitleBarStyle
|
||||
|
||||
public val JewelTheme.Companion.defaultTitleBarStyle: TitleBarStyle
|
||||
@Composable @ReadOnlyComposable get() = LocalTitleBarStyle.current
|
||||
|
||||
public val JewelTheme.Companion.defaultDecoratedWindowStyle: DecoratedWindowStyle
|
||||
@Composable @ReadOnlyComposable get() = LocalDecoratedWindowStyle.current
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.jetbrains.jewel.window
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerButton
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.onPointerEvent
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetbrains.JBR
|
||||
import java.awt.Frame
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.WindowEvent
|
||||
import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.ui.component.Icon
|
||||
import org.jetbrains.jewel.ui.component.IconButton
|
||||
import org.jetbrains.jewel.ui.component.styling.IconButtonStyle
|
||||
import org.jetbrains.jewel.ui.icon.IconKey
|
||||
import org.jetbrains.jewel.ui.painter.PainterHint
|
||||
import org.jetbrains.jewel.ui.painter.PainterProviderScope
|
||||
import org.jetbrains.jewel.ui.painter.PainterSuffixHint
|
||||
import org.jetbrains.jewel.window.styling.TitleBarStyle
|
||||
|
||||
@Composable
|
||||
internal fun DecoratedWindowScope.TitleBarOnLinux(
|
||||
modifier: Modifier = Modifier,
|
||||
gradientStartColor: Color = Color.Unspecified,
|
||||
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
|
||||
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
|
||||
) {
|
||||
var lastPress = 0L
|
||||
val viewConfig = LocalViewConfiguration.current
|
||||
TitleBarImpl(
|
||||
modifier.onPointerEvent(PointerEventType.Press, PointerEventPass.Main) {
|
||||
if (
|
||||
this.currentEvent.button == PointerButton.Primary &&
|
||||
this.currentEvent.changes.any { changed -> !changed.isConsumed }
|
||||
) {
|
||||
JBR.getWindowMove()?.startMovingTogetherWithMouse(window, MouseEvent.BUTTON1)
|
||||
if (
|
||||
System.currentTimeMillis() - lastPress in
|
||||
viewConfig.doubleTapMinTimeMillis..viewConfig.doubleTapTimeoutMillis
|
||||
) {
|
||||
if (state.isMaximized) {
|
||||
window.extendedState = Frame.NORMAL
|
||||
} else {
|
||||
window.extendedState = Frame.MAXIMIZED_BOTH
|
||||
}
|
||||
}
|
||||
lastPress = System.currentTimeMillis()
|
||||
}
|
||||
},
|
||||
gradientStartColor,
|
||||
style,
|
||||
{ _, _ -> PaddingValues(0.dp) },
|
||||
) { state ->
|
||||
CloseButton({ window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) }, state, style)
|
||||
|
||||
if (state.isMaximized) {
|
||||
ControlButton({ window.extendedState = Frame.NORMAL }, state, style.icons.restoreButton, "Restore")
|
||||
} else {
|
||||
ControlButton(
|
||||
{ window.extendedState = Frame.MAXIMIZED_BOTH },
|
||||
state,
|
||||
style.icons.maximizeButton,
|
||||
"Maximize",
|
||||
)
|
||||
}
|
||||
ControlButton({ window.extendedState = Frame.ICONIFIED }, state, style.icons.minimizeButton, "Minimize")
|
||||
content(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleBarScope.CloseButton(
|
||||
onClick: () -> Unit,
|
||||
state: DecoratedWindowState,
|
||||
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
|
||||
) {
|
||||
ControlButton(onClick, state, style.icons.closeButton, "Close", style, style.paneCloseButtonStyle)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleBarScope.ControlButton(
|
||||
onClick: () -> Unit,
|
||||
state: DecoratedWindowState,
|
||||
iconKey: IconKey,
|
||||
description: String,
|
||||
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
|
||||
iconButtonStyle: IconButtonStyle = style.paneButtonStyle,
|
||||
) {
|
||||
IconButton(
|
||||
onClick,
|
||||
Modifier.align(Alignment.End).focusable(false).size(style.metrics.titlePaneButtonSize),
|
||||
style = iconButtonStyle,
|
||||
) {
|
||||
Icon(iconKey, description, hint = if (state.isActive) PainterHint else Inactive)
|
||||
}
|
||||
}
|
||||
|
||||
private data object Inactive : PainterSuffixHint() {
|
||||
override fun PainterProviderScope.suffix(): String = "Inactive"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.jetbrains.jewel.window
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetbrains.JBR
|
||||
import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.window.styling.TitleBarStyle
|
||||
import org.jetbrains.jewel.window.utils.macos.MacUtil
|
||||
|
||||
public fun Modifier.newFullscreenControls(newControls: Boolean = true): Modifier =
|
||||
this then
|
||||
NewFullscreenControlsElement(
|
||||
newControls,
|
||||
debugInspectorInfo {
|
||||
name = "newFullscreenControls"
|
||||
value = newControls
|
||||
},
|
||||
)
|
||||
|
||||
private class NewFullscreenControlsElement(val newControls: Boolean, val inspectorInfo: InspectorInfo.() -> Unit) :
|
||||
ModifierNodeElement<NewFullscreenControlsNode>() {
|
||||
override fun create(): NewFullscreenControlsNode = NewFullscreenControlsNode(newControls)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
val otherModifier = other as? NewFullscreenControlsElement ?: return false
|
||||
return newControls == otherModifier.newControls
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = newControls.hashCode()
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
inspectorInfo()
|
||||
}
|
||||
|
||||
override fun update(node: NewFullscreenControlsNode) {
|
||||
node.newControls = newControls
|
||||
}
|
||||
}
|
||||
|
||||
private class NewFullscreenControlsNode(var newControls: Boolean) : Modifier.Node()
|
||||
|
||||
@Composable
|
||||
internal fun DecoratedWindowScope.TitleBarOnMacOs(
|
||||
modifier: Modifier = Modifier,
|
||||
gradientStartColor: Color = Color.Unspecified,
|
||||
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
|
||||
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
|
||||
) {
|
||||
val newFullscreenControls =
|
||||
modifier.foldOut(false) { e, r ->
|
||||
if (e is NewFullscreenControlsElement) {
|
||||
e.newControls
|
||||
} else {
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
if (newFullscreenControls) {
|
||||
System.setProperty("apple.awt.newFullScreeControls", true.toString())
|
||||
System.setProperty(
|
||||
"apple.awt.newFullScreeControls.background",
|
||||
"${style.colors.fullscreenControlButtonsBackground.toArgb()}",
|
||||
)
|
||||
MacUtil.updateColors(window)
|
||||
} else {
|
||||
System.clearProperty("apple.awt.newFullScreeControls")
|
||||
System.clearProperty("apple.awt.newFullScreeControls.background")
|
||||
}
|
||||
|
||||
val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() }
|
||||
|
||||
TitleBarImpl(
|
||||
modifier = modifier.customTitleBarMouseEventHandler(titleBar),
|
||||
gradientStartColor = gradientStartColor,
|
||||
style = style,
|
||||
applyTitleBar = { height, state ->
|
||||
if (state.isFullscreen) {
|
||||
MacUtil.updateFullScreenButtons(window)
|
||||
}
|
||||
titleBar.height = height.value
|
||||
JBR.getWindowDecorations().setCustomTitleBar(window, titleBar)
|
||||
|
||||
if (state.isFullscreen && newFullscreenControls) {
|
||||
PaddingValues(start = 80.dp)
|
||||
} else {
|
||||
PaddingValues(start = titleBar.leftInset.dp, end = titleBar.rightInset.dp)
|
||||
}
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.jetbrains.jewel.window
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetbrains.JBR
|
||||
import com.jetbrains.WindowDecorations.CustomTitleBar
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.isActive
|
||||
import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.ui.util.isDark
|
||||
import org.jetbrains.jewel.window.styling.TitleBarStyle
|
||||
|
||||
@Composable
|
||||
internal fun DecoratedWindowScope.TitleBarOnWindows(
|
||||
modifier: Modifier = Modifier,
|
||||
gradientStartColor: Color = Color.Unspecified,
|
||||
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
|
||||
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
|
||||
) {
|
||||
val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() }
|
||||
|
||||
TitleBarImpl(
|
||||
modifier = modifier.customTitleBarMouseEventHandler(titleBar),
|
||||
gradientStartColor = gradientStartColor,
|
||||
style = style,
|
||||
applyTitleBar = { height, _ ->
|
||||
titleBar.height = height.value
|
||||
titleBar.putProperty("controls.dark", style.colors.background.isDark())
|
||||
JBR.getWindowDecorations().setCustomTitleBar(window, titleBar)
|
||||
PaddingValues(start = titleBar.leftInset.dp, end = titleBar.rightInset.dp)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Modifier.customTitleBarMouseEventHandler(titleBar: CustomTitleBar): Modifier =
|
||||
pointerInput(Unit) {
|
||||
val currentContext = currentCoroutineContext()
|
||||
awaitPointerEventScope {
|
||||
var inUserControl = false
|
||||
while (currentContext.isActive) {
|
||||
val event = awaitPointerEvent(PointerEventPass.Main)
|
||||
event.changes.forEach {
|
||||
if (!it.isConsumed && !inUserControl) {
|
||||
titleBar.forceHitTest(false)
|
||||
} else {
|
||||
if (event.type == PointerEventType.Press) {
|
||||
inUserControl = true
|
||||
}
|
||||
if (event.type == PointerEventType.Release) {
|
||||
inUserControl = false
|
||||
}
|
||||
titleBar.forceHitTest(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package org.jetbrains.jewel.window
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.isUnspecified
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.MeasurePolicy
|
||||
import androidx.compose.ui.layout.MeasureResult
|
||||
import androidx.compose.ui.layout.MeasureScope
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.node.ParentDataModifierNode
|
||||
import androidx.compose.ui.platform.InspectableValue
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.NoInspectorInfo
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import java.awt.Window
|
||||
import kotlin.math.max
|
||||
import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.foundation.theme.LocalContentColor
|
||||
import org.jetbrains.jewel.foundation.theme.OverrideDarkMode
|
||||
import org.jetbrains.jewel.ui.component.styling.LocalDefaultDropdownStyle
|
||||
import org.jetbrains.jewel.ui.component.styling.LocalIconButtonStyle
|
||||
import org.jetbrains.jewel.ui.util.isDark
|
||||
import org.jetbrains.jewel.window.styling.TitleBarStyle
|
||||
import org.jetbrains.jewel.window.utils.DesktopPlatform
|
||||
import org.jetbrains.jewel.window.utils.macos.MacUtil
|
||||
|
||||
internal const val TITLE_BAR_COMPONENT_LAYOUT_ID_PREFIX = "__TITLE_BAR_"
|
||||
|
||||
internal const val TITLE_BAR_LAYOUT_ID = "__TITLE_BAR_CONTENT__"
|
||||
|
||||
internal const val TITLE_BAR_BORDER_LAYOUT_ID = "__TITLE_BAR_BORDER__"
|
||||
|
||||
@Composable
|
||||
public fun DecoratedWindowScope.TitleBar(
|
||||
modifier: Modifier = Modifier,
|
||||
gradientStartColor: Color = Color.Unspecified,
|
||||
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
|
||||
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
|
||||
) {
|
||||
when (DesktopPlatform.Current) {
|
||||
DesktopPlatform.Linux -> TitleBarOnLinux(modifier, gradientStartColor, style, content)
|
||||
DesktopPlatform.Windows -> TitleBarOnWindows(modifier, gradientStartColor, style, content)
|
||||
DesktopPlatform.MacOS -> TitleBarOnMacOs(modifier, gradientStartColor, style, content)
|
||||
DesktopPlatform.Unknown -> error("TitleBar is not supported on this platform(${System.getProperty("os.name")})")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DecoratedWindowScope.TitleBarImpl(
|
||||
modifier: Modifier = Modifier,
|
||||
gradientStartColor: Color = Color.Unspecified,
|
||||
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
|
||||
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
|
||||
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
|
||||
) {
|
||||
val titleBarInfo = LocalTitleBarInfo.current
|
||||
|
||||
val background by style.colors.backgroundFor(state)
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val backgroundBrush =
|
||||
remember(background, gradientStartColor) {
|
||||
if (gradientStartColor.isUnspecified) {
|
||||
SolidColor(background)
|
||||
} else {
|
||||
with(density) {
|
||||
Brush.horizontalGradient(
|
||||
0.0f to background,
|
||||
0.5f to gradientStartColor,
|
||||
1.0f to background,
|
||||
startX = style.metrics.gradientStartX.toPx(),
|
||||
endX = style.metrics.gradientEndX.toPx(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Layout(
|
||||
content = {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides style.colors.content,
|
||||
LocalIconButtonStyle provides style.iconButtonStyle,
|
||||
LocalDefaultDropdownStyle provides style.dropdownStyle,
|
||||
) {
|
||||
OverrideDarkMode(background.isDark()) {
|
||||
val scope = TitleBarScopeImpl(titleBarInfo.title, titleBarInfo.icon)
|
||||
scope.content(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
modifier
|
||||
.background(backgroundBrush)
|
||||
.focusProperties { canFocus = false }
|
||||
.layoutId(TITLE_BAR_LAYOUT_ID)
|
||||
.height(style.metrics.height)
|
||||
.onSizeChanged { with(density) { applyTitleBar(it.height.toDp(), state) } }
|
||||
.fillMaxWidth(),
|
||||
measurePolicy = rememberTitleBarMeasurePolicy(window, state, applyTitleBar),
|
||||
)
|
||||
|
||||
Spacer(Modifier.layoutId(TITLE_BAR_BORDER_LAYOUT_ID).height(1.dp).fillMaxWidth().background(style.colors.border))
|
||||
}
|
||||
|
||||
internal class TitleBarMeasurePolicy(
|
||||
private val window: Window,
|
||||
private val state: DecoratedWindowState,
|
||||
private val applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
|
||||
) : MeasurePolicy {
|
||||
override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
|
||||
if (measurables.isEmpty()) {
|
||||
return layout(width = constraints.minWidth, height = constraints.minHeight) {}
|
||||
}
|
||||
|
||||
var occupiedSpaceHorizontally = 0
|
||||
|
||||
var maxSpaceVertically = constraints.minHeight
|
||||
val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||
val measuredPlaceable = mutableListOf<Pair<Measurable, Placeable>>()
|
||||
|
||||
for (it in measurables) {
|
||||
val placeable = it.measure(contentConstraints.offset(horizontal = -occupiedSpaceHorizontally))
|
||||
if (constraints.maxWidth < occupiedSpaceHorizontally + placeable.width) {
|
||||
break
|
||||
}
|
||||
occupiedSpaceHorizontally += placeable.width
|
||||
maxSpaceVertically = max(maxSpaceVertically, placeable.height)
|
||||
measuredPlaceable += it to placeable
|
||||
}
|
||||
|
||||
val boxHeight = maxSpaceVertically
|
||||
|
||||
val contentPadding = applyTitleBar(boxHeight.toDp(), state)
|
||||
|
||||
val leftInset = contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
|
||||
val rightInset = contentPadding.calculateRightPadding(layoutDirection).roundToPx()
|
||||
|
||||
occupiedSpaceHorizontally += leftInset
|
||||
occupiedSpaceHorizontally += rightInset
|
||||
|
||||
val boxWidth = maxOf(constraints.minWidth, occupiedSpaceHorizontally)
|
||||
|
||||
return layout(boxWidth, boxHeight) {
|
||||
if (state.isFullscreen) {
|
||||
MacUtil.updateFullScreenButtons(window)
|
||||
}
|
||||
val placeableGroups =
|
||||
measuredPlaceable.groupBy { (measurable, _) ->
|
||||
(measurable.parentData as? TitleBarChildDataNode)?.horizontalAlignment
|
||||
?: Alignment.CenterHorizontally
|
||||
}
|
||||
|
||||
var headUsedSpace = leftInset
|
||||
var trailerUsedSpace = rightInset
|
||||
|
||||
placeableGroups[Alignment.Start]?.forEach { (_, placeable) ->
|
||||
val x = headUsedSpace
|
||||
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
|
||||
placeable.placeRelative(x, y)
|
||||
headUsedSpace += placeable.width
|
||||
}
|
||||
placeableGroups[Alignment.End]?.forEach { (_, placeable) ->
|
||||
val x = boxWidth - placeable.width - trailerUsedSpace
|
||||
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
|
||||
placeable.placeRelative(x, y)
|
||||
trailerUsedSpace += placeable.width
|
||||
}
|
||||
|
||||
val centerPlaceable = placeableGroups[Alignment.CenterHorizontally].orEmpty()
|
||||
|
||||
val requiredCenterSpace = centerPlaceable.sumOf { it.second.width }
|
||||
val minX = headUsedSpace
|
||||
val maxX = boxWidth - trailerUsedSpace - requiredCenterSpace
|
||||
var centerX = (boxWidth - requiredCenterSpace) / 2
|
||||
|
||||
if (minX <= maxX) {
|
||||
if (centerX > maxX) {
|
||||
centerX = maxX
|
||||
}
|
||||
if (centerX < minX) {
|
||||
centerX = minX
|
||||
}
|
||||
|
||||
centerPlaceable.forEach { (_, placeable) ->
|
||||
val x = centerX
|
||||
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
|
||||
placeable.placeRelative(x, y)
|
||||
centerX += placeable.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun rememberTitleBarMeasurePolicy(
|
||||
window: Window,
|
||||
state: DecoratedWindowState,
|
||||
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
|
||||
): MeasurePolicy = remember(window, state, applyTitleBar) { TitleBarMeasurePolicy(window, state, applyTitleBar) }
|
||||
|
||||
public interface TitleBarScope {
|
||||
public val title: String
|
||||
|
||||
public val icon: Painter?
|
||||
|
||||
@Stable public fun Modifier.align(alignment: Alignment.Horizontal): Modifier
|
||||
}
|
||||
|
||||
private class TitleBarScopeImpl(override val title: String, override val icon: Painter?) : TitleBarScope {
|
||||
override fun Modifier.align(alignment: Alignment.Horizontal): Modifier =
|
||||
this then
|
||||
TitleBarChildDataElement(
|
||||
alignment,
|
||||
debugInspectorInfo {
|
||||
name = "align"
|
||||
value = alignment
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private class TitleBarChildDataElement(
|
||||
val horizontalAlignment: Alignment.Horizontal,
|
||||
val inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
|
||||
) : ModifierNodeElement<TitleBarChildDataNode>(), InspectableValue {
|
||||
override fun create(): TitleBarChildDataNode = TitleBarChildDataNode(horizontalAlignment)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
val otherModifier = other as? TitleBarChildDataElement ?: return false
|
||||
return horizontalAlignment == otherModifier.horizontalAlignment
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = horizontalAlignment.hashCode()
|
||||
|
||||
override fun update(node: TitleBarChildDataNode) {
|
||||
node.horizontalAlignment = horizontalAlignment
|
||||
}
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
inspectorInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private class TitleBarChildDataNode(var horizontalAlignment: Alignment.Horizontal) :
|
||||
ParentDataModifierNode, Modifier.Node() {
|
||||
override fun Density.modifyParentData(parentData: Any?) = this@TitleBarChildDataNode
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.jetbrains.jewel.window.styling
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import org.jetbrains.jewel.foundation.GenerateDataFunctions
|
||||
import org.jetbrains.jewel.window.DecoratedWindowState
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class DecoratedWindowStyle(
|
||||
public val colors: DecoratedWindowColors,
|
||||
public val metrics: DecoratedWindowMetrics,
|
||||
) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class DecoratedWindowColors(public val border: Color, public val borderInactive: Color) {
|
||||
@Composable
|
||||
public fun borderFor(state: DecoratedWindowState): State<Color> =
|
||||
rememberUpdatedState(
|
||||
when {
|
||||
!state.isActive -> borderInactive
|
||||
else -> border
|
||||
}
|
||||
)
|
||||
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class DecoratedWindowMetrics(public val borderWidth: Dp) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
public val LocalDecoratedWindowStyle: ProvidableCompositionLocal<DecoratedWindowStyle> = staticCompositionLocalOf {
|
||||
error("No DecoratedWindowStyle provided. Have you forgotten the theme?")
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.jetbrains.jewel.window.styling
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import org.jetbrains.jewel.foundation.GenerateDataFunctions
|
||||
import org.jetbrains.jewel.ui.component.styling.DropdownStyle
|
||||
import org.jetbrains.jewel.ui.component.styling.IconButtonStyle
|
||||
import org.jetbrains.jewel.ui.icon.IconKey
|
||||
import org.jetbrains.jewel.window.DecoratedWindowState
|
||||
|
||||
@Stable
|
||||
@GenerateDataFunctions
|
||||
public class TitleBarStyle(
|
||||
public val colors: TitleBarColors,
|
||||
public val metrics: TitleBarMetrics,
|
||||
public val icons: TitleBarIcons,
|
||||
public val dropdownStyle: DropdownStyle,
|
||||
public val iconButtonStyle: IconButtonStyle,
|
||||
public val paneButtonStyle: IconButtonStyle,
|
||||
public val paneCloseButtonStyle: IconButtonStyle,
|
||||
) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class TitleBarColors(
|
||||
public val background: Color,
|
||||
public val inactiveBackground: Color,
|
||||
public val content: Color,
|
||||
public val border: Color,
|
||||
// The background color for newControlButtons(three circles in left top corner) in MacOS
|
||||
// fullscreen mode
|
||||
public val fullscreenControlButtonsBackground: Color,
|
||||
// The hover and press background color for window control buttons(minimize, maximize) in Linux
|
||||
public val titlePaneButtonHoveredBackground: Color,
|
||||
public val titlePaneButtonPressedBackground: Color,
|
||||
// The hover and press background color for window close button in Linux
|
||||
public val titlePaneCloseButtonHoveredBackground: Color,
|
||||
public val titlePaneCloseButtonPressedBackground: Color,
|
||||
// The hover and press background color for IconButtons in title bar content
|
||||
public val iconButtonHoveredBackground: Color,
|
||||
public val iconButtonPressedBackground: Color,
|
||||
// The hover and press background color for Dropdown in title bar content
|
||||
public val dropdownPressedBackground: Color,
|
||||
public val dropdownHoveredBackground: Color,
|
||||
) {
|
||||
@Composable
|
||||
public fun backgroundFor(state: DecoratedWindowState): State<Color> =
|
||||
rememberUpdatedState(
|
||||
when {
|
||||
!state.isActive -> inactiveBackground
|
||||
else -> background
|
||||
}
|
||||
)
|
||||
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class TitleBarMetrics(
|
||||
public val height: Dp,
|
||||
public val gradientStartX: Dp,
|
||||
public val gradientEndX: Dp,
|
||||
public val titlePaneButtonSize: DpSize,
|
||||
) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class TitleBarIcons(
|
||||
public val minimizeButton: IconKey,
|
||||
public val maximizeButton: IconKey,
|
||||
public val restoreButton: IconKey,
|
||||
public val closeButton: IconKey,
|
||||
) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
public val LocalTitleBarStyle: ProvidableCompositionLocal<TitleBarStyle> = staticCompositionLocalOf {
|
||||
error("No TitleBarStyle provided. Have you forgotten the theme?")
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jetbrains.jewel.window.utils
|
||||
|
||||
public enum class DesktopPlatform {
|
||||
Linux,
|
||||
Windows,
|
||||
MacOS,
|
||||
Unknown;
|
||||
|
||||
public companion object {
|
||||
public val Current: DesktopPlatform by lazy {
|
||||
val name = System.getProperty("os.name")
|
||||
when {
|
||||
name?.startsWith("Linux") == true -> Linux
|
||||
name?.startsWith("Win") == true -> Windows
|
||||
name == "Mac OS X" -> MacOS
|
||||
else -> Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.jetbrains.jewel.window.utils
|
||||
|
||||
import com.sun.jna.Native
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
internal object JnaLoader {
|
||||
private var loaded: Boolean? = null
|
||||
private val logger = Logger.getLogger(JnaLoader::class.java.simpleName)
|
||||
|
||||
@Synchronized
|
||||
fun load() {
|
||||
if (loaded == null) {
|
||||
loaded = false
|
||||
try {
|
||||
val time = measureTimeMillis { Native.POINTER_SIZE }
|
||||
logger.info("JNA library (${Native.POINTER_SIZE shl 3}-bit) loaded in $time ms")
|
||||
loaded = true
|
||||
} catch (@Suppress("TooGenericExceptionCaught") t: Throwable) {
|
||||
logger.log(
|
||||
Level.WARNING,
|
||||
"Unable to load JNA library(os=${
|
||||
System.getProperty("os.name")
|
||||
} ${System.getProperty("os.version")}, jna.boot.library.path=${
|
||||
System.getProperty("jna.boot.library.path")
|
||||
})",
|
||||
t,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val isLoaded: Boolean
|
||||
get() {
|
||||
if (loaded == null) {
|
||||
load()
|
||||
}
|
||||
return loaded ?: false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.jetbrains.jewel.window.utils
|
||||
|
||||
import java.lang.reflect.AccessibleObject
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import sun.misc.Unsafe
|
||||
|
||||
internal object UnsafeAccessing {
|
||||
private val logger = Logger.getLogger(UnsafeAccessing::class.java.simpleName)
|
||||
|
||||
private val unsafe: Any? by lazy {
|
||||
try {
|
||||
val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe")
|
||||
theUnsafe.isAccessible = true
|
||||
theUnsafe.get(null) as Unsafe
|
||||
} catch (@Suppress("TooGenericExceptionCaught") error: Throwable) {
|
||||
logger.log(Level.WARNING, "Unsafe accessing initializing failed.", error)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val desktopModule by lazy { ModuleLayer.boot().findModule("java.desktop").get() }
|
||||
|
||||
val ownerModule: Module by lazy { this.javaClass.module }
|
||||
|
||||
private val isAccessibleFieldOffset: Long? by lazy {
|
||||
try {
|
||||
(unsafe as? Unsafe)?.objectFieldOffset(Parent::class.java.getDeclaredField("first"))
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val implAddOpens by lazy {
|
||||
try {
|
||||
Module::class.java.getDeclaredMethod("implAddOpens", String::class.java, Module::class.java).accessible()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun assignAccessibility(obj: AccessibleObject) {
|
||||
try {
|
||||
val theUnsafe = unsafe as? Unsafe ?: return
|
||||
val offset = isAccessibleFieldOffset ?: return
|
||||
theUnsafe.putBooleanVolatile(obj, offset, true)
|
||||
} catch (_: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
fun assignAccessibility(module: Module, packages: List<String>) {
|
||||
try {
|
||||
packages.forEach { implAddOpens?.invoke(module, it, ownerModule) }
|
||||
} catch (_: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private class Parent {
|
||||
var first = false
|
||||
|
||||
@Volatile var second: Any? = null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T : AccessibleObject> T.accessible(): T = apply { UnsafeAccessing.assignAccessibility(this) }
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.jetbrains.jewel.window.utils.macos
|
||||
|
||||
import com.sun.jna.Function
|
||||
import com.sun.jna.Library
|
||||
import com.sun.jna.Native
|
||||
import com.sun.jna.Pointer
|
||||
import java.lang.reflect.Proxy
|
||||
import java.util.Arrays
|
||||
import java.util.Collections
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import org.jetbrains.jewel.window.utils.JnaLoader
|
||||
|
||||
internal object Foundation {
|
||||
private val logger = Logger.getLogger(Foundation::class.java.simpleName)
|
||||
|
||||
init {
|
||||
if (!JnaLoader.isLoaded) {
|
||||
logger.log(Level.WARNING, "JNA is not loaded")
|
||||
}
|
||||
}
|
||||
|
||||
private val myFoundationLibrary: FoundationLibrary? by lazy {
|
||||
try {
|
||||
Native.load("Foundation", FoundationLibrary::class.java, Collections.singletonMap("jna.encoding", "UTF8"))
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val myObjcMsgSend: Function? by lazy {
|
||||
try {
|
||||
(Proxy.getInvocationHandler(myFoundationLibrary) as Library.Handler)
|
||||
.nativeLibrary
|
||||
.getFunction("objc_msgSend")
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the ID of the NSClass with className */
|
||||
fun getObjcClass(className: String?): ID? = myFoundationLibrary?.objc_getClass(className)
|
||||
|
||||
fun getProtocol(name: String?): ID? = myFoundationLibrary?.objc_getProtocol(name)
|
||||
|
||||
fun createSelector(s: String?): Pointer? = myFoundationLibrary?.sel_registerName(s)
|
||||
|
||||
private fun prepInvoke(id: ID?, selector: Pointer?, args: Array<out Any?>): Array<Any?> {
|
||||
val invokArgs = arrayOfNulls<Any>(args.size + 2)
|
||||
invokArgs[0] = id
|
||||
invokArgs[1] = selector
|
||||
System.arraycopy(args, 0, invokArgs, 2, args.size)
|
||||
return invokArgs
|
||||
}
|
||||
|
||||
// objc_msgSend is called with the calling convention of the target method
|
||||
// on x86_64 this does not make a difference, but arm64 uses a different calling convention for
|
||||
// varargs
|
||||
// it is therefore important to not call objc_msgSend as a vararg function
|
||||
operator fun invoke(id: ID?, selector: Pointer?, vararg args: Any?): ID =
|
||||
ID(myObjcMsgSend?.invokeLong(prepInvoke(id, selector, args)) ?: 0)
|
||||
|
||||
/**
|
||||
* Invokes the given vararg selector. Expects `NSArray arrayWithObjects:(id), ...` like signature, i.e. exactly one
|
||||
* fixed argument, followed by varargs.
|
||||
*/
|
||||
fun invokeVarArg(id: ID?, selector: Pointer?, vararg args: Any?): ID {
|
||||
// c functions and objc methods have at least 1 fixed argument, we therefore need to
|
||||
// separate out the first argument
|
||||
return myFoundationLibrary?.objc_msgSend(id, selector, args[0], *Arrays.copyOfRange(args, 1, args.size))
|
||||
?: ID.NIL
|
||||
}
|
||||
|
||||
operator fun invoke(cls: String?, selector: String?, vararg args: Any?): ID =
|
||||
invoke(getObjcClass(cls), createSelector(selector), *args)
|
||||
|
||||
fun invokeVarArg(cls: String?, selector: String?, vararg args: Any?): ID =
|
||||
invokeVarArg(getObjcClass(cls), createSelector(selector), *args)
|
||||
|
||||
fun safeInvoke(stringCls: String?, stringSelector: String?, vararg args: Any?): ID {
|
||||
val cls = getObjcClass(stringCls)
|
||||
val selector = createSelector(stringSelector)
|
||||
if (!invoke(cls, "respondsToSelector:", selector).booleanValue()) {
|
||||
error("Missing selector $stringSelector for $stringCls")
|
||||
}
|
||||
return invoke(cls, selector, *args)
|
||||
}
|
||||
|
||||
operator fun invoke(id: ID?, selector: String?, vararg args: Any?): ID = invoke(id, createSelector(selector), *args)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.jetbrains.jewel.window.utils.macos
|
||||
|
||||
import com.sun.jna.Callback
|
||||
import com.sun.jna.Library
|
||||
import com.sun.jna.Pointer
|
||||
|
||||
@Suppress(
|
||||
"ktlint:standard:function-naming",
|
||||
"ktlint:standard:property-naming",
|
||||
"Unused",
|
||||
"FunctionName",
|
||||
"ConstPropertyName",
|
||||
) // Borrowed code
|
||||
internal interface FoundationLibrary : Library {
|
||||
fun NSLog(pString: Pointer?, thing: Any?)
|
||||
|
||||
fun NSFullUserName(): ID?
|
||||
|
||||
fun objc_allocateClassPair(supercls: ID?, name: String?, extraBytes: Int): ID?
|
||||
|
||||
fun objc_registerClassPair(cls: ID?)
|
||||
|
||||
fun CFStringCreateWithBytes(
|
||||
allocator: Pointer?,
|
||||
bytes: ByteArray?,
|
||||
byteCount: Int,
|
||||
encoding: Int,
|
||||
isExternalRepresentation: Byte,
|
||||
): ID?
|
||||
|
||||
fun CFStringGetCString(theString: ID?, buffer: ByteArray?, bufferSize: Int, encoding: Int): Byte
|
||||
|
||||
fun CFStringGetLength(theString: ID?): Int
|
||||
|
||||
fun CFStringConvertNSStringEncodingToEncoding(nsEncoding: Long): Long
|
||||
|
||||
fun CFStringConvertEncodingToIANACharSetName(cfEncoding: Long): ID?
|
||||
|
||||
fun CFStringConvertIANACharSetNameToEncoding(encodingName: ID?): Long
|
||||
|
||||
fun CFStringConvertEncodingToNSStringEncoding(cfEncoding: Long): Long
|
||||
|
||||
fun CFRetain(cfTypeRef: ID?)
|
||||
|
||||
fun CFRelease(cfTypeRef: ID?)
|
||||
|
||||
fun CFGetRetainCount(cfTypeRef: Pointer?): Int
|
||||
|
||||
fun objc_getClass(className: String?): ID?
|
||||
|
||||
fun objc_getProtocol(name: String?): ID?
|
||||
|
||||
fun class_createInstance(pClass: ID?, extraBytes: Int): ID?
|
||||
|
||||
fun sel_registerName(selectorName: String?): Pointer?
|
||||
|
||||
fun class_replaceMethod(cls: ID?, selName: Pointer?, impl: Callback?, types: String?): ID?
|
||||
|
||||
fun objc_getMetaClass(name: String?): ID?
|
||||
|
||||
/**
|
||||
* Note: Vararg version. Should only be used only for selectors with a single fixed argument followed by varargs.
|
||||
*/
|
||||
fun objc_msgSend(receiver: ID?, selector: Pointer?, firstArg: Any?, vararg args: Any?): ID?
|
||||
|
||||
fun class_respondsToSelector(cls: ID?, selName: Pointer?): Boolean
|
||||
|
||||
fun class_addMethod(cls: ID?, selName: Pointer?, imp: Callback?, types: String?): Boolean
|
||||
|
||||
fun class_addMethod(cls: ID?, selName: Pointer?, imp: ID?, types: String?): Boolean
|
||||
|
||||
fun class_addProtocol(aClass: ID?, protocol: ID?): Boolean
|
||||
|
||||
fun class_isMetaClass(cls: ID?): Boolean
|
||||
|
||||
fun NSStringFromSelector(selector: Pointer?): ID?
|
||||
|
||||
fun NSStringFromClass(aClass: ID?): ID?
|
||||
|
||||
fun objc_getClass(clazz: Pointer?): Pointer?
|
||||
|
||||
companion object {
|
||||
const val kCFStringEncodingMacRoman = 0
|
||||
const val kCFStringEncodingWindowsLatin1 = 0x0500
|
||||
const val kCFStringEncodingISOLatin1 = 0x0201
|
||||
const val kCFStringEncodingNextStepLatin = 0x0B01
|
||||
const val kCFStringEncodingASCII = 0x0600
|
||||
const val kCFStringEncodingUnicode = 0x0100
|
||||
const val kCFStringEncodingUTF8 = 0x08000100
|
||||
const val kCFStringEncodingNonLossyASCII = 0x0BFF
|
||||
const val kCFStringEncodingUTF16 = 0x0100
|
||||
const val kCFStringEncodingUTF16BE = 0x10000100
|
||||
const val kCFStringEncodingUTF16LE = 0x14000100
|
||||
const val kCFStringEncodingUTF32 = 0x0c000100
|
||||
const val kCFStringEncodingUTF32BE = 0x18000100
|
||||
const val kCFStringEncodingUTF32LE = 0x1c000100
|
||||
|
||||
// https://developer.apple.com/library/mac/documentation/Carbon/Reference/CGWindow_Reference/Constants/Constants.html#//apple_ref/doc/constant_group/Window_List_Option_Constants
|
||||
const val kCGWindowListOptionAll = 0
|
||||
const val kCGWindowListOptionOnScreenOnly = 1
|
||||
const val kCGWindowListOptionOnScreenAboveWindow = 2
|
||||
const val kCGWindowListOptionOnScreenBelowWindow = 4
|
||||
const val kCGWindowListOptionIncludingWindow = 8
|
||||
const val kCGWindowListExcludeDesktopElements = 16
|
||||
|
||||
// https://developer.apple.com/library/mac/documentation/Carbon/Reference/CGWindow_Reference/Constants/Constants.html#//apple_ref/doc/constant_group/Window_Image_Types
|
||||
const val kCGWindowImageDefault = 0
|
||||
const val kCGWindowImageBoundsIgnoreFraming = 1
|
||||
const val kCGWindowImageShouldBeOpaque = 2
|
||||
const val kCGWindowImageOnlyShadows = 4
|
||||
const val kCGWindowImageBestResolution = 8
|
||||
const val kCGWindowImageNominalResolution = 16
|
||||
|
||||
// see enum NSBitmapImageFileType
|
||||
const val NSBitmapImageFileTypeTIFF = 0
|
||||
const val NSBitmapImageFileTypeBMP = 1
|
||||
const val NSBitmapImageFileTypeGIF = 2
|
||||
const val NSBitmapImageFileTypeJPEG = 3
|
||||
const val NSBitmapImageFileTypePNG = 4
|
||||
const val NSBitmapImageFileTypeJPEG2000 = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.jetbrains.jewel.window.utils.macos
|
||||
|
||||
import com.sun.jna.NativeLong
|
||||
|
||||
/** Could be an address in memory (if pointer to a class or method) or a value (like 0 or 1) */
|
||||
@Suppress("OVERRIDE_DEPRECATION") // Copied code
|
||||
internal class ID : NativeLong {
|
||||
constructor()
|
||||
|
||||
constructor(peer: Long) : super(peer)
|
||||
|
||||
fun booleanValue(): Boolean = toInt() != 0
|
||||
|
||||
override fun toByte(): Byte = toInt().toByte()
|
||||
|
||||
override fun toChar(): Char = toInt().toChar()
|
||||
|
||||
override fun toShort(): Short = toInt().toShort()
|
||||
|
||||
@Suppress("RedundantOverride") // Without this, we get a SOE
|
||||
override fun toInt(): Int = super.toInt()
|
||||
|
||||
companion object {
|
||||
@JvmField val NIL = ID(0L)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.jetbrains.jewel.window.utils.macos
|
||||
|
||||
import java.awt.Component
|
||||
import java.awt.Window
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import javax.swing.SwingUtilities
|
||||
import org.jetbrains.jewel.window.utils.UnsafeAccessing
|
||||
import org.jetbrains.jewel.window.utils.accessible
|
||||
|
||||
internal object MacUtil {
|
||||
private val logger = Logger.getLogger(MacUtil::class.java.simpleName)
|
||||
|
||||
init {
|
||||
try {
|
||||
UnsafeAccessing.assignAccessibility(
|
||||
UnsafeAccessing.desktopModule,
|
||||
listOf("sun.awt", "sun.lwawt", "sun.lwawt.macosx"),
|
||||
)
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
logger.log(Level.WARNING, "Assign access for jdk.desktop failed.", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getWindowFromJavaWindow(w: Window?): ID {
|
||||
if (w == null) {
|
||||
return ID.NIL
|
||||
}
|
||||
try {
|
||||
val cPlatformWindow = getPlatformWindow(w)
|
||||
if (cPlatformWindow != null) {
|
||||
val ptr = cPlatformWindow.javaClass.superclass.getDeclaredField("ptr")
|
||||
ptr.setAccessible(true)
|
||||
return ID(ptr.getLong(cPlatformWindow))
|
||||
}
|
||||
} catch (e: IllegalAccessException) {
|
||||
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
|
||||
} catch (e: NoSuchFieldException) {
|
||||
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
|
||||
}
|
||||
return ID.NIL
|
||||
}
|
||||
|
||||
fun getPlatformWindow(w: Window): Any? {
|
||||
try {
|
||||
val awtAccessor = Class.forName("sun.awt.AWTAccessor")
|
||||
val componentAccessor = awtAccessor.getMethod("getComponentAccessor").invoke(null)
|
||||
val getPeer = componentAccessor.javaClass.getMethod("getPeer", Component::class.java).accessible()
|
||||
val peer = getPeer.invoke(componentAccessor, w)
|
||||
if (peer != null) {
|
||||
val cWindowPeerClass: Class<*> = peer.javaClass
|
||||
val getPlatformWindowMethod = cWindowPeerClass.getDeclaredMethod("getPlatformWindow")
|
||||
val cPlatformWindow = getPlatformWindowMethod.invoke(peer)
|
||||
if (cPlatformWindow != null) {
|
||||
return cPlatformWindow
|
||||
}
|
||||
}
|
||||
} catch (e: NoSuchMethodException) {
|
||||
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
|
||||
} catch (e: IllegalAccessException) {
|
||||
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
|
||||
} catch (e: InvocationTargetException) {
|
||||
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun updateColors(w: Window) {
|
||||
SwingUtilities.invokeLater {
|
||||
val window = getWindowFromJavaWindow(w)
|
||||
val delegate = Foundation.invoke(window, "delegate")
|
||||
if (
|
||||
Foundation.invoke(delegate, "respondsToSelector:", Foundation.createSelector("updateColors"))
|
||||
.booleanValue()
|
||||
) {
|
||||
Foundation.invoke(delegate, "updateColors")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFullScreenButtons(w: Window) {
|
||||
SwingUtilities.invokeLater {
|
||||
val selector = Foundation.createSelector("updateFullScreenButtons")
|
||||
val window = getWindowFromJavaWindow(w)
|
||||
val delegate = Foundation.invoke(window, "delegate")
|
||||
|
||||
if (Foundation.invoke(delegate, "respondsToSelector:", selector).booleanValue()) {
|
||||
Foundation.invoke(delegate, "updateFullScreenButtons")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
platform/jewel/detekt.yml
Normal file
189
platform/jewel/detekt.yml
Normal file
@@ -0,0 +1,189 @@
|
||||
build:
|
||||
maxIssues: 0
|
||||
excludeCorrectable: false
|
||||
weights:
|
||||
# complexity: 2
|
||||
# LongParameterList: 1
|
||||
# style: 1
|
||||
# comments: 1
|
||||
|
||||
config:
|
||||
validation: true
|
||||
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
|
||||
excludes: ''
|
||||
|
||||
processors:
|
||||
active: true
|
||||
exclude:
|
||||
- 'DetektProgressListener'
|
||||
# - 'FunctionCountProcessor'
|
||||
# - 'PropertyCountProcessor'
|
||||
# - 'ClassCountProcessor'
|
||||
# - 'PackageCountProcessor'
|
||||
# - 'KtFileCountProcessor'
|
||||
|
||||
console-reports:
|
||||
active: true
|
||||
exclude:
|
||||
- 'ProjectStatisticsReport'
|
||||
- 'ComplexityReport'
|
||||
- 'NotificationReport'
|
||||
# - 'FindingsReport'
|
||||
- 'FileBasedFindingsReport'
|
||||
|
||||
comments:
|
||||
active: false
|
||||
|
||||
complexity:
|
||||
active: true
|
||||
LongParameterList:
|
||||
active: false
|
||||
TooManyFunctions:
|
||||
active: false
|
||||
LongMethod:
|
||||
active: false
|
||||
CyclomaticComplexMethod:
|
||||
active: false
|
||||
|
||||
coroutines:
|
||||
active: true
|
||||
GlobalCoroutineUsage:
|
||||
active: true
|
||||
RedundantSuspendModifier:
|
||||
active: true
|
||||
SleepInsteadOfDelay:
|
||||
active: true
|
||||
SuspendFunWithFlowReturnType:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
active: true
|
||||
NotImplementedDeclaration:
|
||||
active: true
|
||||
ObjectExtendsThrowable:
|
||||
active: true
|
||||
|
||||
performance:
|
||||
active: true
|
||||
SpreadOperator:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ]
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
DontDowncastCollectionTypes:
|
||||
active: true
|
||||
ExitOutsideMain:
|
||||
active: true
|
||||
HasPlatformType:
|
||||
active: true
|
||||
IgnoredReturnValue:
|
||||
active: true
|
||||
ImplicitUnitReturnType:
|
||||
active: true
|
||||
allowExplicitReturnType: true
|
||||
MapGetWithNotNullAssertionOperator:
|
||||
active: true
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: true
|
||||
UnreachableCatchBlock:
|
||||
active: true
|
||||
UselessPostfixExpression:
|
||||
active: true
|
||||
|
||||
style:
|
||||
active: true
|
||||
CollapsibleIfStatements:
|
||||
active: true
|
||||
DataClassShouldBeImmutable:
|
||||
active: true
|
||||
EqualsOnSignatureLine:
|
||||
active: true
|
||||
ExpressionBodySyntax:
|
||||
active: true
|
||||
includeLineWrapping: false
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
comments:
|
||||
- value: 'STOPSHIP'
|
||||
reason: 'Forbidden STOPSHIP marker in comment, please address before shipping'
|
||||
allowedPatterns: ''
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
maxJumpCount: 3
|
||||
MagicNumber:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ]
|
||||
ignoreNumbers: [ '-1', '0', '1', '2' ]
|
||||
ignoreHashCodeFunction: true
|
||||
ignorePropertyDeclaration: true
|
||||
ignoreLocalVariableDeclaration: true
|
||||
ignoreConstantDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotation: true
|
||||
ignoreNamedArgument: true
|
||||
ignoreEnums: true
|
||||
ignoreRanges: true
|
||||
MandatoryBracesLoops:
|
||||
active: true
|
||||
MaxLineLength:
|
||||
active: true
|
||||
maxLineLength: 150
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
excludeCommentStatements: false
|
||||
NoTabs:
|
||||
active: true
|
||||
OptionalUnit:
|
||||
active: true
|
||||
PreferToOverPairSyntax:
|
||||
active: true
|
||||
RedundantExplicitType:
|
||||
active: true
|
||||
ReturnCount:
|
||||
active: false
|
||||
SpacingBetweenPackageAndImports:
|
||||
active: true
|
||||
ThrowsCount:
|
||||
active: true
|
||||
max: 5
|
||||
TrailingWhitespace:
|
||||
active: true
|
||||
UnnecessaryLet:
|
||||
active: true
|
||||
UnnecessaryParentheses:
|
||||
active: false
|
||||
UnnecessaryAbstractClass:
|
||||
ignoreAnnotated:
|
||||
- Module
|
||||
UntilInsteadOfRangeTo:
|
||||
active: true
|
||||
UnusedImports:
|
||||
active: true
|
||||
UnusedPrivateMember:
|
||||
# We need to disable this otherwise we'd need to @Suppress all Composable previews...
|
||||
active: false
|
||||
UseArrayLiteralsInAnnotations:
|
||||
active: true
|
||||
UseCheckNotNull:
|
||||
active: true
|
||||
UseCheckOrError:
|
||||
active: true
|
||||
UseEmptyCounterpart:
|
||||
active: true
|
||||
UseIfEmptyOrIfBlank:
|
||||
active: true
|
||||
UseIsNullOrEmpty:
|
||||
active: true
|
||||
UseRequire:
|
||||
active: true
|
||||
UseRequireNotNull:
|
||||
active: true
|
||||
VarCouldBeVal:
|
||||
active: true
|
||||
|
||||
naming:
|
||||
FunctionNaming:
|
||||
active: false
|
||||
ignoreAnnotated:
|
||||
- Composable
|
||||
1022
platform/jewel/foundation/api/foundation.api
Normal file
1022
platform/jewel/foundation/api/foundation.api
Normal file
File diff suppressed because it is too large
Load Diff
19
platform/jewel/foundation/build.gradle.kts
Normal file
19
platform/jewel/foundation/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
import org.jetbrains.compose.ComposeBuildConfig
|
||||
|
||||
plugins {
|
||||
jewel
|
||||
`jewel-publish`
|
||||
`jewel-check-public-api`
|
||||
alias(libs.plugins.composeDesktop)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
private val composeVersion
|
||||
get() = ComposeBuildConfig.composeVersion
|
||||
|
||||
dependencies {
|
||||
api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion")
|
||||
|
||||
testImplementation(compose.desktop.uiTestJUnit4)
|
||||
testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
/**
|
||||
* Enables the new compositing strategy for rendering directly into Swing Graphics. This fixes z-order problems and
|
||||
* artifacts on resizing, but has a performance penalty when using infinitely repeating animations.
|
||||
*
|
||||
* We assume the majority of our users will want this flag to be on, so this convenience function is provided to that
|
||||
* end. Make sure you call it **before** you initialize any Compose content. The function is idempotent and extremely
|
||||
* cheap, so you can call it on any entry point.
|
||||
*/
|
||||
@ExperimentalJewelApi
|
||||
public fun enableNewSwingCompositing() {
|
||||
System.setProperty("compose.swing.render.on.graphics", "true")
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
import kotlin.RequiresOptIn.Level
|
||||
|
||||
@RequiresOptIn(
|
||||
level = Level.WARNING,
|
||||
message = "This is an experimental API for Jewel and is likely to change before becoming stable.",
|
||||
)
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.CONSTRUCTOR,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
public annotation class ExperimentalJewelApi
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
/**
|
||||
* Instructs the Poko compiler plugin to generate equals, hashcode, and toString functions for the class it's attached
|
||||
* to.
|
||||
*
|
||||
* See [this issue](https://github.com/JetBrains/jewel/issues/83) for details.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS) public annotation class GenerateDataFunctions
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class GlobalColors(
|
||||
public val borders: BorderColors,
|
||||
public val outlines: OutlineColors,
|
||||
public val text: TextColors,
|
||||
public val panelBackground: Color,
|
||||
) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class TextColors(
|
||||
public val normal: Color,
|
||||
public val selected: Color,
|
||||
public val disabled: Color,
|
||||
public val info: Color,
|
||||
public val error: Color,
|
||||
) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class BorderColors(public val normal: Color, public val focused: Color, public val disabled: Color) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class OutlineColors(
|
||||
public val focused: Color,
|
||||
public val focusedWarning: Color,
|
||||
public val focusedError: Color,
|
||||
public val warning: Color,
|
||||
public val error: Color,
|
||||
) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
public val LocalGlobalColors: ProvidableCompositionLocal<GlobalColors> = staticCompositionLocalOf {
|
||||
error("No GlobalColors provided. Have you forgotten the theme?")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class GlobalMetrics(public val outlineWidth: Dp, public val rowHeight: Dp) {
|
||||
public companion object
|
||||
}
|
||||
|
||||
public val LocalGlobalMetrics: ProvidableCompositionLocal<GlobalMetrics> = staticCompositionLocalOf {
|
||||
error("No GlobalMetrics provided. Have you forgotten the theme?")
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
import kotlin.RequiresOptIn.Level
|
||||
|
||||
@RequiresOptIn(
|
||||
level = Level.WARNING,
|
||||
message = "This is an internal API for Jewel and is subject to change without notice.",
|
||||
)
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.CONSTRUCTOR,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
public annotation class InternalJewelApi
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
|
||||
internal fun RoundRect.grow(delta: Float) =
|
||||
RoundRect(
|
||||
left = left - delta,
|
||||
top = top - delta,
|
||||
right = right + delta,
|
||||
bottom = bottom + delta,
|
||||
topLeftCornerRadius = CornerRadius(topLeftCornerRadius.x + delta, topLeftCornerRadius.y + delta),
|
||||
topRightCornerRadius = CornerRadius(topRightCornerRadius.x + delta, topRightCornerRadius.y + delta),
|
||||
bottomLeftCornerRadius = CornerRadius(bottomLeftCornerRadius.x + delta, bottomLeftCornerRadius.y + delta),
|
||||
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x + delta, bottomRightCornerRadius.y + delta),
|
||||
)
|
||||
|
||||
internal fun RoundRect.shrink(delta: Float) =
|
||||
RoundRect(
|
||||
left = left + delta,
|
||||
top = top + delta,
|
||||
right = right - delta,
|
||||
bottom = bottom - delta,
|
||||
topLeftCornerRadius = CornerRadius(topLeftCornerRadius.x - delta, topLeftCornerRadius.y - delta),
|
||||
topRightCornerRadius = CornerRadius(topRightCornerRadius.x - delta, topRightCornerRadius.y - delta),
|
||||
bottomLeftCornerRadius = CornerRadius(bottomLeftCornerRadius.x - delta, bottomLeftCornerRadius.y - delta),
|
||||
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x - delta, bottomRightCornerRadius.y - delta),
|
||||
)
|
||||
|
||||
internal fun RoundRect.hasAtLeastOneNonRoundedCorner() =
|
||||
topLeftCornerRadius.x == 0f && topLeftCornerRadius.y == 0f ||
|
||||
topRightCornerRadius.x == 0f && topRightCornerRadius.y == 0f ||
|
||||
bottomLeftCornerRadius.x == 0f && bottomLeftCornerRadius.y == 0f ||
|
||||
bottomRightCornerRadius.x == 0f && bottomRightCornerRadius.y == 0f
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.jetbrains.jewel.foundation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.isUnspecified
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
public sealed class Stroke {
|
||||
@Immutable
|
||||
public object None : Stroke() {
|
||||
override fun toString(): String = "None"
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class Solid
|
||||
internal constructor(
|
||||
public val width: Dp,
|
||||
public val color: Color,
|
||||
public val alignment: Alignment,
|
||||
public val expand: Dp,
|
||||
) : Stroke()
|
||||
|
||||
@Immutable
|
||||
@GenerateDataFunctions
|
||||
public class Brush
|
||||
internal constructor(
|
||||
public val width: Dp,
|
||||
public val brush: androidx.compose.ui.graphics.Brush,
|
||||
public val alignment: Alignment,
|
||||
public val expand: Dp,
|
||||
) : Stroke()
|
||||
|
||||
public enum class Alignment {
|
||||
Inside,
|
||||
Center,
|
||||
Outside,
|
||||
}
|
||||
}
|
||||
|
||||
public fun Stroke(width: Dp, color: Color, alignment: Stroke.Alignment, expand: Dp = Dp.Unspecified): Stroke {
|
||||
if (width.value == 0f) return Stroke.None
|
||||
if (color.isUnspecified) return Stroke.None
|
||||
|
||||
return Stroke.Solid(width, color, alignment, expand)
|
||||
}
|
||||
|
||||
public fun Stroke(width: Dp, brush: Brush, alignment: Stroke.Alignment, expand: Dp = Dp.Unspecified): Stroke {
|
||||
if (width.value == 0f) return Stroke.None
|
||||
return when (brush) {
|
||||
is SolidColor -> {
|
||||
if (brush.value.isUnspecified) {
|
||||
Stroke.None
|
||||
} else {
|
||||
Stroke.Solid(width, brush.value, alignment, expand)
|
||||
}
|
||||
}
|
||||
else -> Stroke.Brush(width, brush, alignment, expand)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jetbrains.jewel.foundation.actionSystem
|
||||
|
||||
public interface DataProviderContext {
|
||||
public fun <TValue : Any> set(key: String, value: TValue?)
|
||||
|
||||
public fun <TValue : Any> lazy(key: String, initializer: () -> TValue?)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jetbrains.jewel.foundation.actionSystem
|
||||
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
|
||||
internal class DataProviderElement(val dataProvider: DataProviderContext.() -> Unit) :
|
||||
ModifierNodeElement<DataProviderNode>() {
|
||||
override fun create(): DataProviderNode = DataProviderNode(dataProvider)
|
||||
|
||||
override fun update(node: DataProviderNode) {
|
||||
node.dataProvider = dataProvider
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DataProviderElement
|
||||
|
||||
return dataProvider == other.dataProvider
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = dataProvider.hashCode()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.jetbrains.jewel.foundation.actionSystem
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusEventModifierNode
|
||||
import androidx.compose.ui.focus.FocusState
|
||||
import androidx.compose.ui.node.TraversableNode
|
||||
|
||||
public class DataProviderNode(public var dataProvider: DataProviderContext.() -> Unit) :
|
||||
Modifier.Node(), FocusEventModifierNode, TraversableNode {
|
||||
public var hasFocus: Boolean = false
|
||||
|
||||
override fun onFocusEvent(focusState: FocusState) {
|
||||
hasFocus = focusState.hasFocus
|
||||
}
|
||||
|
||||
override val traverseKey: TraverseKey = TraverseKey
|
||||
|
||||
public companion object TraverseKey
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jetbrains.jewel.foundation.actionSystem
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusEventModifierNode
|
||||
|
||||
/**
|
||||
* Configure component to provide data for IntelliJ Actions system.
|
||||
*
|
||||
* Use this modifier to provide context related data that can be used by IntelliJ Actions functionality such as Search
|
||||
* Everywhere, Action Popups etc.
|
||||
*
|
||||
* Important note: modifiers order is important, so be careful with order of [focusable] and [provideData] (see
|
||||
* [FocusEventModifierNode]).
|
||||
*
|
||||
* This can be traversed from Modifier.Node() using Compose traversal API using DataProviderNode as a TraverseKey
|
||||
*/
|
||||
@Suppress("unused")
|
||||
public fun Modifier.provideData(dataProvider: DataProviderContext.() -> Unit): Modifier =
|
||||
this then DataProviderElement(dataProvider)
|
||||
@@ -0,0 +1,389 @@
|
||||
package org.jetbrains.jewel.foundation.code
|
||||
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.AGSL
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.DART
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.JSON
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.KOTLIN
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.PYTHON
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.RUST
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.TYPESCRIPT
|
||||
import org.jetbrains.jewel.foundation.code.MimeType.Known.YAML
|
||||
|
||||
/**
|
||||
* Represents the language and dialect of a source snippet, as an RFC 2046 mime type.
|
||||
*
|
||||
* For example, a Kotlin source file may have the mime type `text/kotlin`. However, if it corresponds to a
|
||||
* `build.gradle.kts` file, we'll also attach the mime parameter `role=gradle`, resulting in mime type `text/kotlin;
|
||||
* role=gradle`.
|
||||
*
|
||||
* For XML resource files, we'll attach other attributes; for example `role=manifest` for Android manifest files and
|
||||
* `role=resource` for XML resource files. For the latter we may also attach for example `folderType=values`, and for
|
||||
* XML files in general, the root tag, such as `text/xml; role=resource; folderType=layout; rootTag=LinearLayout`.
|
||||
*
|
||||
* This class does not implement *all* aspects of the RFC; in particular, we don't treat attributes as case-insensitive,
|
||||
* and we only support value tokens, not value strings -- neither of these are needed for our purposes.
|
||||
*
|
||||
* This is implemented using a value class, such that behind the scenes we're really just passing a String around. This
|
||||
* also means we can't initialize related values such as the corresponding Markdown fenced block names, or IntelliJ
|
||||
* language id's. Instead, these are looked up via `when`-tables. When adding a new language, update all lookup methods:
|
||||
* * [displayName]
|
||||
*/
|
||||
@JvmInline
|
||||
public value class MimeType(private val mimeType: String) {
|
||||
public fun displayName(): String =
|
||||
when (normalizeString()) {
|
||||
Known.KOTLIN.mimeType -> if (isGradle()) "Gradle DSL" else "Kotlin"
|
||||
Known.JAVA.mimeType -> "Java"
|
||||
Known.XML.mimeType -> {
|
||||
when (getRole()) {
|
||||
null -> "XML"
|
||||
VALUE_MANIFEST -> "Manifest"
|
||||
VALUE_RESOURCE -> {
|
||||
val folderType = getAttribute(ATTR_FOLDER_TYPE)
|
||||
folderType?.capitalizeAsciiOnly() ?: "XML"
|
||||
}
|
||||
|
||||
else -> "XML"
|
||||
}
|
||||
}
|
||||
|
||||
Known.JSON.mimeType -> "JSON"
|
||||
Known.TEXT.mimeType -> "Text"
|
||||
Known.REGEX.mimeType -> "Regular Expression"
|
||||
Known.GROOVY.mimeType -> if (isGradle()) "Gradle" else "Groovy"
|
||||
Known.TOML.mimeType -> if (isVersionCatalog()) "Version Catalog" else "TOML"
|
||||
Known.C.mimeType -> "C"
|
||||
Known.CPP.mimeType -> "C++"
|
||||
Known.SVG.mimeType -> "SVG"
|
||||
Known.AIDL.mimeType -> "AIDL"
|
||||
Known.SQL.mimeType -> "SQL"
|
||||
Known.PROGUARD.mimeType -> "Shrinker Config"
|
||||
Known.PROPERTIES.mimeType -> "Properties"
|
||||
Known.PROTO.mimeType -> "Protobuf"
|
||||
Known.PYTHON.mimeType -> "Python"
|
||||
Known.DART.mimeType -> "Dart"
|
||||
Known.RUST.mimeType -> "Rust"
|
||||
Known.JAVASCRIPT.mimeType -> "JavaScript"
|
||||
Known.AGSL.mimeType -> "Android Graphics Shading Language"
|
||||
Known.SHELL.mimeType -> "Shell Script"
|
||||
Known.YAML.mimeType -> "YAML"
|
||||
Known.GO.mimeType -> "Go"
|
||||
else -> mimeType
|
||||
}
|
||||
|
||||
private fun normalizeString(): String {
|
||||
when (this) {
|
||||
// Built-ins are already normalized, don't do string and sorting work
|
||||
Known.KOTLIN,
|
||||
Known.JAVA,
|
||||
Known.TEXT,
|
||||
Known.XML,
|
||||
Known.PROPERTIES,
|
||||
Known.TOML,
|
||||
Known.JSON,
|
||||
Known.REGEX,
|
||||
Known.GROOVY,
|
||||
Known.C,
|
||||
Known.CPP,
|
||||
Known.SVG,
|
||||
Known.AIDL,
|
||||
Known.PROTO,
|
||||
Known.SQL,
|
||||
Known.PROGUARD,
|
||||
Known.MANIFEST,
|
||||
Known.RESOURCE,
|
||||
Known.GRADLE,
|
||||
Known.GRADLE_KTS,
|
||||
Known.VERSION_CATALOG,
|
||||
Known.PYTHON,
|
||||
Known.DART,
|
||||
Known.RUST,
|
||||
Known.JAVASCRIPT,
|
||||
Known.TYPESCRIPT,
|
||||
Known.AGSL,
|
||||
Known.SHELL,
|
||||
Known.YAML,
|
||||
Known.GO,
|
||||
Known.UNKNOWN -> return this.mimeType
|
||||
}
|
||||
|
||||
val baseEnd = mimeType.indexOf(';')
|
||||
val normalizedBase =
|
||||
when (val base = if (baseEnd == -1) mimeType else mimeType.substring(0, baseEnd)) {
|
||||
"text/x-java-source",
|
||||
"application/x-java",
|
||||
"text/x-java" -> Known.JAVA.mimeType
|
||||
|
||||
"application/kotlin-source",
|
||||
"text/x-kotlin",
|
||||
"text/x-kotlin-source" -> KOTLIN.mimeType
|
||||
|
||||
"application/xml" -> Known.XML.mimeType
|
||||
"application/json",
|
||||
"application/vnd.api+json",
|
||||
"application/hal+json",
|
||||
"application/ld+json" -> JSON.mimeType
|
||||
|
||||
"image/svg+xml" -> Known.XML.mimeType
|
||||
"text/x-python",
|
||||
"application/x-python-script" -> PYTHON.mimeType
|
||||
|
||||
"text/dart",
|
||||
"text/x-dart",
|
||||
"application/dart",
|
||||
"application/x-dart" -> DART.mimeType
|
||||
|
||||
"application/javascript",
|
||||
"application/x-javascript",
|
||||
"text/ecmascript",
|
||||
"application/ecmascript",
|
||||
"application/x-ecmascript" -> Known.JAVASCRIPT.mimeType
|
||||
|
||||
"application/typescript" + "application/x-typescript" -> TYPESCRIPT.mimeType
|
||||
"text/x-rust",
|
||||
"application/x-rust" -> RUST.mimeType
|
||||
|
||||
"text/x-sksl" -> AGSL.mimeType
|
||||
"application/yaml",
|
||||
"text/x-yaml",
|
||||
"application/x-yaml" -> YAML.mimeType
|
||||
|
||||
else -> base
|
||||
}
|
||||
|
||||
if (baseEnd == -1) {
|
||||
return normalizedBase
|
||||
}
|
||||
|
||||
val attributes =
|
||||
mimeType
|
||||
.split(';')
|
||||
.asSequence()
|
||||
.drop(1)
|
||||
.sorted()
|
||||
.mapNotNull {
|
||||
val index = it.indexOf('=')
|
||||
if (index != -1) {
|
||||
it.substring(0, index).trim() to it.substring(index + 1).trim()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.filter { isRelevantAttribute(it.first) }
|
||||
.map { "${it.first}=${it.second}" }
|
||||
.joinToString("; ")
|
||||
|
||||
return if (attributes.isNotBlank()) {
|
||||
"$normalizedBase; $attributes"
|
||||
} else {
|
||||
normalizedBase
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether the given attribute should be included in a normalized string */
|
||||
private fun isRelevantAttribute(attribute: String): Boolean =
|
||||
when (attribute) {
|
||||
ATTR_ROLE,
|
||||
ATTR_ROOT_TAG,
|
||||
ATTR_FOLDER_TYPE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns just the language portion of the mime type.
|
||||
*
|
||||
* For example, for `text/kotlin; role=gradle` this will return `text/kotlin`. For `text/plain; charset=us-ascii`
|
||||
* this returns `text/plain`
|
||||
*/
|
||||
public fun base(): MimeType = MimeType(mimeType.substringBefore(';').trim())
|
||||
|
||||
internal fun getRole(): String? = getAttribute(ATTR_ROLE)
|
||||
|
||||
private fun getFolderType(): String? = getAttribute(ATTR_FOLDER_TYPE)
|
||||
|
||||
private fun getAttribute(name: String): String? {
|
||||
val marker = "$name="
|
||||
var start = mimeType.indexOf(marker)
|
||||
if (start == -1) {
|
||||
return null
|
||||
}
|
||||
start += marker.length
|
||||
var end = start
|
||||
while (end < mimeType.length && !mimeType[end].isWhitespace()) {
|
||||
end++
|
||||
}
|
||||
return mimeType.substring(start, end).removeSurrounding("\"")
|
||||
}
|
||||
|
||||
override fun toString(): String = mimeType
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
* Attribute used to indicate the role this source file plays; for example, an XML file may be a "manifest" or a
|
||||
* "resource".
|
||||
*/
|
||||
const val ATTR_ROLE: String = "role"
|
||||
|
||||
/** For XML resource files, the folder type if any (such as "values" or "layout") */
|
||||
const val ATTR_FOLDER_TYPE: String = "folderType"
|
||||
|
||||
/** For XML files, the root tag in the content */
|
||||
const val ATTR_ROOT_TAG: String = "rootTag"
|
||||
|
||||
const val VALUE_RESOURCE = "resource"
|
||||
const val VALUE_MANIFEST = "manifest"
|
||||
}
|
||||
|
||||
public object Known {
|
||||
// Well known mime types for major languages.
|
||||
|
||||
/**
|
||||
* Well known name for Kotlin source snippets. This is the base mime type; consider using [isKotlin] instead to
|
||||
* check if a mime type represents Kotlin code such that it also picks up `build.gradle.kts` files (which carry
|
||||
* extra attributes in the mime type; see [GRADLE_KTS].)
|
||||
*/
|
||||
public val KOTLIN: MimeType = MimeType("text/kotlin")
|
||||
|
||||
/** Well known name for Java source snippets. */
|
||||
public val JAVA: MimeType = MimeType("text/java")
|
||||
|
||||
/** Well known mime type for text files. */
|
||||
public val TEXT: MimeType = MimeType("text/plain")
|
||||
|
||||
/**
|
||||
* Special marker mimetype for unknown or unspecified mime types. These will generally be treated as [TEXT] for
|
||||
* editor purposes. (The standard "unknown" mime type is application/octet-stream (from RFC 2046) but we know
|
||||
* this isn't binary data; it's text.)
|
||||
*
|
||||
* Note that [MimeType] is generally nullable in places where it's optional instead of being set to this value,
|
||||
* but this mime type is there for places where we need a specific value to point to.
|
||||
*/
|
||||
public val UNKNOWN: MimeType = MimeType("text/unknown")
|
||||
|
||||
/**
|
||||
* Well known name for XML source snippets. This is the base mime type; consider using [isXml] instead to check
|
||||
* if a mime type represents any XML such that it also picks up manifest files, resource files etc., which all
|
||||
* carry extra attributes in the mime type; see for example [MANIFEST] and [RESOURCE].
|
||||
*/
|
||||
public val XML: MimeType = MimeType("text/xml")
|
||||
public val PROPERTIES: MimeType = MimeType("text/properties")
|
||||
public val TOML: MimeType = MimeType("text/toml")
|
||||
public val JSON: MimeType = MimeType("text/json")
|
||||
public val REGEX: MimeType = MimeType("text/x-regex-source")
|
||||
public val GROOVY: MimeType = MimeType("text/groovy")
|
||||
public val C: MimeType = MimeType("text/c")
|
||||
public val CPP: MimeType = MimeType("text/c++")
|
||||
public val SVG: MimeType = MimeType("image/svg+xml")
|
||||
public val AIDL: MimeType = MimeType("text/x-aidl-source")
|
||||
public val PROTO: MimeType = MimeType("text/x-protobuf")
|
||||
public val SQL: MimeType = MimeType("text/x-sql")
|
||||
public val PROGUARD: MimeType = MimeType("text/x-proguard")
|
||||
public val PYTHON: MimeType = MimeType("text/python")
|
||||
public val JAVASCRIPT: MimeType = MimeType("text/javascript")
|
||||
public val TYPESCRIPT: MimeType = MimeType("text/typescript")
|
||||
public val DART: MimeType = MimeType("application/dart")
|
||||
public val RUST: MimeType = MimeType("text/rust")
|
||||
public val AGSL: MimeType = MimeType("text/x-agsl")
|
||||
public val SHELL: MimeType = MimeType("application/x-sh")
|
||||
public val YAML: MimeType = MimeType("text/yaml")
|
||||
public val GO: MimeType = MimeType("text/go")
|
||||
|
||||
/** Note that most resource files will also have a folder type, so don't use equality on this mime type */
|
||||
public val RESOURCE: MimeType = MimeType("$XML; $ATTR_ROLE=resource")
|
||||
public val MANIFEST: MimeType = MimeType("$XML;$ATTR_ROLE=manifest $ATTR_ROOT_TAG=manifest")
|
||||
public val GRADLE: MimeType = MimeType("$GROOVY; $ATTR_ROLE=gradle")
|
||||
public val GRADLE_KTS: MimeType = MimeType("$KOTLIN; $ATTR_ROLE=gradle")
|
||||
public val VERSION_CATALOG: MimeType = MimeType("$TOML; $ATTR_ROLE=version-catalog")
|
||||
|
||||
/** Maps from a markdown language [name] back to a mime type. */
|
||||
public fun fromMarkdownLanguageName(name: String): MimeType? =
|
||||
when (name) {
|
||||
"kotlin",
|
||||
"kt",
|
||||
"kts" -> KOTLIN
|
||||
|
||||
"java" -> JAVA
|
||||
"xml" -> XML
|
||||
"json",
|
||||
"json5" -> JSON
|
||||
|
||||
"regex",
|
||||
"regexp" -> REGEX
|
||||
|
||||
"groovy" -> GROOVY
|
||||
"toml" -> TOML
|
||||
"c" -> C
|
||||
"c++" -> CPP
|
||||
"svg" -> SVG
|
||||
"aidl" -> AIDL
|
||||
"sql" -> SQL
|
||||
"properties" -> PROPERTIES
|
||||
"protobuf" -> PROTO
|
||||
"python2",
|
||||
"python3",
|
||||
"py",
|
||||
"python" -> PYTHON
|
||||
|
||||
"dart" -> DART
|
||||
"rust" -> RUST
|
||||
"js",
|
||||
"javascript" -> JAVASCRIPT
|
||||
|
||||
"typescript" -> TYPESCRIPT
|
||||
"sksl" -> AGSL
|
||||
"sh",
|
||||
"bash",
|
||||
"zsh",
|
||||
"shell" -> SHELL
|
||||
|
||||
"yaml",
|
||||
"yml" -> YAML
|
||||
|
||||
"go",
|
||||
"golang" -> YAML
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Is the base language for this mime type Kotlin? */
|
||||
public fun MimeType?.isKotlin(): Boolean = this?.base() == MimeType.Known.KOTLIN
|
||||
|
||||
/** Is the base language for this mime type Java? */
|
||||
public fun MimeType?.isJava(): Boolean = this?.base() == MimeType.Known.JAVA
|
||||
|
||||
/** Is the base language for this mime type XML? */
|
||||
public fun MimeType?.isXml(): Boolean = this?.base() == MimeType.Known.XML
|
||||
|
||||
/** Is this a Gradle file (which could be in Groovy, *or*, Kotlin) */
|
||||
public fun MimeType?.isGradle(): Boolean = this?.getRole() == "gradle"
|
||||
|
||||
/** Is this a version catalog file (which could be in TOML, or in Groovy) */
|
||||
public fun MimeType?.isVersionCatalog(): Boolean = this?.getRole() == "version-catalog"
|
||||
|
||||
/** Is this an Android manifest file? */
|
||||
public fun MimeType?.isManifest(): Boolean = this?.getRole() == "manifest"
|
||||
|
||||
/** Is the base language for this mime type SQL? */
|
||||
public fun MimeType?.isSql(): Boolean = this?.base() == MimeType.Known.SQL
|
||||
|
||||
/** Is the base language for this mime type a regular expression? */
|
||||
public fun MimeType?.isRegex(): Boolean = this?.base() == MimeType.Known.REGEX
|
||||
|
||||
/** Is the base language for this mime type a protobuf? */
|
||||
public fun MimeType?.isProto(): Boolean = this?.base() == MimeType.Known.PROTO
|
||||
|
||||
private fun String.capitalizeAsciiOnly(): String {
|
||||
if (isEmpty()) return this
|
||||
val c = this[0]
|
||||
return if (c in 'a'..'z') {
|
||||
buildString(length) {
|
||||
append(c.uppercaseChar())
|
||||
append(this@capitalizeAsciiOnly, 1, this@capitalizeAsciiOnly.length)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.jetbrains.jewel.foundation.code.highlighting
|
||||
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
|
||||
import org.jetbrains.jewel.foundation.code.MimeType
|
||||
|
||||
@ExperimentalJewelApi
|
||||
public interface CodeHighlighter {
|
||||
/**
|
||||
* Highlights [code] according to rules for the language specified by [mimeType], and returns flow of styled
|
||||
* strings. For basic highlighters with rigid color schemes it is enough to return a flow of one element:
|
||||
* ```
|
||||
* return flowOf(highlightedString(code, mimeType))
|
||||
* ```
|
||||
*
|
||||
* However, some implementations might want gradual highlighting (for example, apply something simple while waiting
|
||||
* for the extensive info from server), or they might rely upon a color scheme that can change at any time.
|
||||
*
|
||||
* In such cases, they need to produce more than one styled string for the same piece of code, and that's when flows
|
||||
* come in handy.
|
||||
*
|
||||
* @see [NoOpCodeHighlighter]
|
||||
*/
|
||||
public fun highlight(code: String, mimeType: MimeType): Flow<AnnotatedString>
|
||||
}
|
||||
|
||||
public val LocalCodeHighlighter: ProvidableCompositionLocal<CodeHighlighter> = staticCompositionLocalOf {
|
||||
NoOpCodeHighlighter
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.jetbrains.jewel.foundation.code.highlighting
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.jetbrains.jewel.foundation.code.MimeType
|
||||
|
||||
public object NoOpCodeHighlighter : CodeHighlighter {
|
||||
override fun highlight(code: String, mimeType: MimeType): Flow<AnnotatedString> = flowOf(AnnotatedString(code))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user