1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

Port to Godot 4 (#900)

* Initial conversion

* Hide some dialogs

* Update addons

* Fix errors in scripts

Temporarily commented out adding new file import types and certain Control methods that are not available in the Window class, such as get_global_mouse_position()

* Update shaders

* Fix some more errors and signals, rename "pressed" to "button_pressed"

* Even more error fixes and renaming corrections

* Fixed more errors, Pixelorama almost runs

* Update ValueSlider.gd

* Remove lock() and unlock(), more ImageTexture.create_from_image() static usage

* More static function using

* Re-add some of the dialog signals, fix window transparency

* Change instances of popup_hide to visibility_changed

* More more errors and warnings

* Fix more errors and warnings

* Get rid of errors in the output when opening Pixelorama in Godot

* Properly connect most signals without using strings

* Fix some scenes

* Don't load Main.tscn

* Emit signals directly instead of using strings

* Fix Keychain menu nodes

* Get rid of self. on most instances, as setters and getters are now always called

* Some more static typing

* Disable texture filters

* Fix zooming

* Fix int as enum warnings

* Fix tools and rename doubleclick to double_click

* Update tool scenes

* Fix tabs

* Fix create new image

* Use static typing on flood fill to speed it up

* Update static-checks.yml

* Reverts #729 for a speedup, hopefully the bug won't get re-introduced

* Fix TransparentChecker warning

* Re-add Default template

* Fix 3D cels

* Fix rotation

Project converted bug. Should be fixed by https://github.com/godotengine/godot/pull/79264

* Fix UITransparency alignment issue, thanks Variable

* Add missing OptionButton items

Hopefully that should be all of them

* Fix the appearance of CollapsibleContainer

* Change instances of world to world_3d

* Fix tool button backgrounds

* Fix Splash dialog

* Fix brush selection

* Update Main.gd

* Fix About Dialog

* Fix more zooming issues

* Fix canvas preview zooming

* Use signals for queue_redraw on project change

* Fix layer button's look

* Fix gradients

* Some gradient fixes and code cleanups, dithering is still broken

* Fix bucket

* Fix the rest of the undo_redo.add_(un)do_method() cases

* Fix guides

* Fix guide text

* Some small changes in Main

* Update Tools.gd

* Fix palette importing

* Get rid of TODOGODOT4s

* Fix the rest of the dialogs

* Update the rest of the scenes

* Fix onion skinning and frame tag dialogs

* Fix file brushes being imported twice

* Fix palette swatch crashing on double click

* Use nearest filter for some of the windows

* Remove old .tres font files

* Fix language switching

* Get rid of Keychain.action_get_first_key() on the extra shortcuts of tools

* Get rid of Keychain.MenuInputActions and directly set shortcuts to the menu items

This temporarily removes echoing support for undo and redo, this will be re-added once https://github.com/godotengine/godot/pull/36493 or https://github.com/godotengine/godot/pull/64317 is merged.

* Clean shortcut-related duplicate code in TopMenuContainer

* Remove DroidSansFallback now that system fonts can be used as fallback

* Remove 3.x settings from project.godot

* Format

* Format gdgifexporter

* Reset Keychain to its original state

* Remove textures from the dark and gray themes

* Remove all textures from the dark theme

* Better static typing in DrawingAlgos

* Use Vector2i for project size

* [Risky commit] Use Vector2i instead of Vector2 for tools

I tested it and everything seems to be working the same as before, but more testing would be appreciated.

* Format after previous commit

* Fix line angle constraint being rotated 180 degrees

This is not a regression from the previous commit(s), Godot 4 probably reversed the logic of `angle_to_point()`.

* Fix input map action not found errors when pressing Shift or Control

* Make AnimatePanel bigger, add spring interpolation

* Fix some layouts/extensions/preferences loading errors

* Fix dithering

* Update layout resources

Probably doesn't change anything at all, but I suppose it might be a good thing to do

* Small changes

* Disable filter in ResizeCanvas dialog

* Fix some preferences default button states

* Fix tile mode always having masking on

* Use integers in tile mode

* Fix checkboxes in preferences not working

* More statically typed arrays!

No need to have these # Array of X comments anymore!

* Fix "apply all" for multiple preview dialogs

* Update theme.tres

* Add HeaderSmall theme type variation

* Fix dynamics buttons

* Don't allow sub-zero zoom values

* Let zoom_out_max always remain Vector2(0.01, 0.01)

This fixes zooming on large canvases

* Bump version to v1.0-dev

* Fix ambient light not working on 3D cels

* Fix .obj loading

* Don't allow greater than max values in the zoom slider

* Set maximum zoom value to always be (500, 500)

* Set zoom slider minimum value to 1

* Some UI changes, mostly related to buttons and the timeline

* Change window titles to what they were before

* [COMPATIBILITY BROKEN WITH v0.x] Fix loading 3D cels

* Avoid changing Cel3DObject's file_path if it's the same

* Make preferences window bigger

* Fix png exporting

* Fix reference image initial size and filter setting

* Fix perspective line reverse scaling on zoom and facing 180 degrees away from cursor

* Format and some linting

* Remove most Images from the rest of the themes

* Remove all textures from all themes

* Fix drawing when the mouse gets released outside the canvas boundaries

* Format Keychain

* Implement #890

* Fix recorder

* Fix layout deletion

* Better static typing and fix empty_clicked signal-connected methods not having arguments

* Fix layout and extension directory creation if they don't already exist

* Change all instances of "HTML5" to "Web"

OS.get_name() now returns "Web" instead of "HTML5" in Godot 4

* Fix JavaScript detection

Opening files in the Web version does not yet work for some reason

* Fix formatting

* Fix lint errors

* Remove unneeded lines from rotation shaders

* Clean some rotation shader related code

* Remove ErrorManager from #891, as it's no longer needed in Godot 4

* Some docstrings

* More Vector2i and Recti replacing their float counterparts

* Remove the hardcoded shortcut from ValueSlider

Note that the brush size shortcut may not change properly because Keychain isn't updated to support Godot 4's input map properly yet.

* Fix bugs from the rebase, integer zooming is currently broken

* Format

* Fix bug where some imported images would fail to load when using smart slice

* Fix integer zooming (I think)

* Fix errors after #898

* Fix some UI issues with PreviewDialog

* Use ctrl/command instead of just ctrl in the shortcuts of the InputMap - gets rid of the need for _use_osx_shortcuts() in Main.gd

* Update Keychain and addons/README.md

* Update CI to Godot 4.1.1 (probably will not work)

* Remove XDGDataPaths.gd

* Make windows non-exclusive

* Attempt to fix macOS CI

* Attempt to fix CI

* Attempt to fix CI

* Minor fix in the dark theme, more will follow

* Silence enumerator/integer warning

* Attempt to fix macOS CI

* Another attempt to fix macOS CI

* Attempt to fix Windows & macOS CI

* fix: Recorder directory create (#903)

* Update Keychain so that the brush size shortcuts can be changed

This update generally lets users use modifier buttons (control, alt, shift, meta) with mouse buttons

* Change OSX to macOS

* Detect if multi-threading is enabled when exporting gifs

* Fix color picker not working on the top color mode

* Make some public methods private in Export.gd

* Remove Global.window_title variable

* Fix frame UI in the timeline breaking after 100 frames

* Static typing improvements for the timeline

* Better static typing for grids

* Fix typo

* Fix pixel grid not appearing

* Move preference updating code to Global using setters

This should make the code a bit more readable, as the logic for each property update can be found directly under the variable declaration, and not hidden in PreferencesDialog's preference_update() method. This also allows for changing these properties outside the preferences, if that will ever be needed. In theory it's also faster as we don't have to do all these string comparisons anymore, but I doubt this will be noticeable in practice.

* Remove RestoreDefaultButton.tscn

* Implement changing font size in the preferences

* Resize HeaderSmall font size along with the default font size

* A step towards fixing image loading in the Web version

Doesn't completely fix the issue, it requires a fix from Godot's side as well

* Implement missing input event actions for buttons

TODO: Add default shortcuts

* Do not change language and theme if they are already the defaults

Reduces the initial loading time a bit

* Remove update_hint_tooltips() as it's no longer needed, only keep it for tools

This was needed in order for hint tooltips to be successfully translated while taking the shortcuts into account. Godot 4 already it correctly for us now. Tools still need it because they contain multiple shortcut data in their tooltips.

* Change ExportDialog's PathDialog's file mode to be "open directory" and make them bigger

* Fix Vector2i + Vector2 errors in grid center snapping

* Update tooltips when the shortcut profile changes

* Fix copy-paste mistake

* Update tooltips during startup if the shortcut profile is not the default

* Fix gif warning label size in ExportDialog

* Fix BBCode in ExportDialog

* Fix some Godot 4.2 warnings

* Some CI fixes

* Static typing improvements and more inline functions

* Format

* Even more static typing, inline methods, docstrings etc

* Some more static typing improvements and inline setters

* Remove unneeded project type specifying

* Fix splash dialog error

* Fix enumerator warning

* Don't preload the font in the rules and guides

* Fix some integer division warnings

Sometimes we indeed need them to be floats

* Change some Rect2s to Rect2is

* Minor static typing improvements

* Update README, CHANGELOG, Translations

* Only load translation files when needed, reduces loading time a bit

* Update Keychain so it doesn't load languages during startup

* Lazy load all tool scenes, breaks compatibility with the extension API

Decreases initial loading time

* Format

* Very minor loading time speedups

* Remove unneeded project type specifying

* Even more static typing and docstring improvements

* Fix extension loading

* Palette docstrings

---------

Co-authored-by: ppphp <kevinniub@gmail.com>
This commit is contained in:
Emmanouil Papadeas 2023-09-04 16:29:06 +03:00 committed by GitHub
parent b2c157fa07
commit 91bfef16b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
486 changed files with 18815 additions and 28658 deletions

View file

@ -1,5 +1,7 @@
disable: disable:
- no-elif-return - no-elif-return
- no-else-return - no-else-return
- max-returns
- private-method-call
max-file-lines: 2000 max-file-lines: 2000

View file

@ -10,15 +10,12 @@ on:
- "installer/*.pot" - "installer/*.pot"
- "installer/po/*" - "installer/po/*"
concurrency: concurrency:
group: ci-${{github.actor}}-${{github.head_ref || github.run_number}}-${{github.ref}}-devdesktop group: ci-${{github.actor}}-${{github.head_ref || github.run_number}}-${{github.ref}}-devdesktop
cancel-in-progress: true cancel-in-progress: true
env: env:
GODOT_VERSION: 3.5.2 GODOT_VERSION: 4.1.1
GODOT_VERSION_MAC: 3.5.2
RASPBERRY_PI_BUILDS_VERSION: 1.15.0
EXPORT_NAME: Pixelorama EXPORT_NAME: Pixelorama
jobs: jobs:
@ -26,7 +23,7 @@ jobs:
name: Windows Export 🗔 name: Windows Export 🗔
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: docker://barichello/godot-ci:3.5.2 image: docker://barichello/godot-ci:4.1.1
steps: steps:
- name: Setup WINE and rcedit 🍷 - name: Setup WINE and rcedit 🍷
run: | run: |
@ -35,19 +32,19 @@ jobs:
wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe
mkdir -v -p ~/.local/share/rcedit mkdir -v -p ~/.local/share/rcedit
mv rcedit-x64.exe ~/.local/share/rcedit mv rcedit-x64.exe ~/.local/share/rcedit
godot -q godot --headless --quit
echo 'export/windows/wine = "/usr/bin/wine"' >> ~/.config/godot/editor_settings-3.tres echo 'export/windows/wine = "/usr/bin/wine"' >> ~/.config/godot/editor_settings-4.tres
echo 'export/windows/rcedit = "/github/home/.local/share/rcedit/rcedit-x64.exe"' >> ~/.config/godot/editor_settings-3.tres echo 'export/windows/rcedit = "/github/home/.local/share/rcedit/rcedit-x64.exe"' >> ~/.config/godot/editor_settings-4.tres
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Setup 💻 - name: Setup 💻
run: | run: |
mkdir -v -p build/windows-64bit ~/.local/share/godot/templates mkdir -v -p build/windows-64bit ~/.local/share/godot/export_templates
mv /root/.local/share/godot/templates/${GODOT_VERSION}.stable ~/.local/share/godot/templates/${GODOT_VERSION}.stable mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable
- name: Windows Build 🔧 - name: Windows Build 🔧
run: godot -v --export "Windows Desktop 64-bit" ./build/windows-64bit/$EXPORT_NAME.exe run: godot --headless -v --export-release "Windows Desktop 64-bit" ./build/windows-64bit/$EXPORT_NAME.exe
- name: Copy pixelorama_data folder 📁 - name: Copy pixelorama_data folder 📁
run: | run: |
cp -R ./pixelorama_data ./build/windows-64bit cp -R ./pixelorama_data ./build/windows-64bit
@ -63,7 +60,7 @@ jobs:
name: Linux Export 🐧 name: Linux Export 🐧
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: docker://barichello/godot-ci:3.5.2 image: docker://barichello/godot-ci:4.1.1
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -71,42 +68,28 @@ jobs:
submodules: true submodules: true
- name: Setup 💻 - name: Setup 💻
run: | run: |
mkdir -v -p build/linux-64bit build/linux-rpi4 ~/.local/share/godot/templates mkdir -v -p build/linux-64bit ~/.local/share/godot/export_templates
mv /root/.local/share/godot/templates/${GODOT_VERSION}.stable ~/.local/share/godot/templates/${GODOT_VERSION}.stable mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable
- name: Download Unofficial Godot Raspberry Pi 4 Builds 🍇
run: |
wget https://github.com/hiulit/Unofficial-Godot-Engine-Raspberry-Pi/releases/download/v${RASPBERRY_PI_BUILDS_VERSION}/godot_${GODOT_VERSION}-stable_rpi4.zip
unzip -a godot_${GODOT_VERSION}-stable_rpi4.zip
- name: Linux Build 🔧 - name: Linux Build 🔧
run: | run: |
godot -v --export "Linux/X11 64-bit" ./build/linux-64bit/$EXPORT_NAME.x86_64 godot --headless -v --export-release "Linux/X11 64-bit" ./build/linux-64bit/$EXPORT_NAME.x86_64
godot -v --export "Raspberry Pi 4" ./build/linux-rpi4/$EXPORT_NAME.rpi4
- name: Give execute permission ☑️ - name: Give execute permission ☑️
run: | run: |
chmod +x ./build/linux-64bit/$EXPORT_NAME.x86_64 chmod +x ./build/linux-64bit/$EXPORT_NAME.x86_64
chmod +x ./build/linux-rpi4/$EXPORT_NAME.rpi4
- name: Copy pixelorama_data folder 📁 - name: Copy pixelorama_data folder 📁
run: | run: |
rm ./pixelorama_data/.gdignore rm ./pixelorama_data/.gdignore
cp -R ./pixelorama_data ./build/linux-64bit cp -R ./pixelorama_data ./build/linux-64bit
cp -R ./pixelorama_data ./build/linux-rpi4
- name: Create tar.gz archive 🗜️ - name: Create tar.gz archive 🗜️
run: | run: |
cd build cd build
tar zcvf linux-64bit.tar.gz linux-64bit tar zcvf linux-64bit.tar.gz linux-64bit
tar zcvf linux-rpi4.tar.gz linux-rpi4
- name: Upload Artifact 🚀 - name: Upload Artifact 🚀
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: Linux-64bit name: Linux-64bit
path: ./build/linux-64bit.tar.gz path: ./build/linux-64bit.tar.gz
retention-days: 14 retention-days: 14
- name: Upload Raspberry Pi 4 Artifact 🚀
uses: actions/upload-artifact@v3
with:
name: Linux-rpi4
path: ./build/linux-rpi4.tar.gz
retention-days: 14
export-mac: export-mac:
name: Mac Export 🍎 name: Mac Export 🍎
@ -118,25 +101,25 @@ jobs:
submodules: true submodules: true
- name: Setup environment 🔧 - name: Setup environment 🔧
run: | run: |
export GODOT_VERSION=${GODOT_VERSION_MAC} export GODOT_VERSION=${GODOT_VERSION}
export EXPORT_NAME=Pixelorama export EXPORT_NAME=Pixelorama
- name: Download and extract export templates 💾 - name: Download and extract export templates 💾
run: | run: |
mkdir -v -p ~/.local/share/godot/templates/${GODOT_VERSION_MAC}.stable mkdir -v -p "/Users/runner/Library/Application Support/Godot/export_templates/${GODOT_VERSION}.stable"
curl -O https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION_MAC}/Godot_v${GODOT_VERSION_MAC}-stable_export_templates.tpz curl -O https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION}/Godot_v${GODOT_VERSION}-stable_export_templates.tpz
unzip -a Godot_v${GODOT_VERSION_MAC}-stable_export_templates.tpz unzip -a Godot_v${GODOT_VERSION}-stable_export_templates.tpz
mv ./templates/* ~/.local/share/godot/templates/${GODOT_VERSION_MAC}.stable mv ./templates/* "/Users/runner/Library/Application Support/Godot/export_templates/${GODOT_VERSION}.stable"
- name: Download Godot headless binary 🤖 - name: Download Godot headless binary 🤖
run: | run: |
wget https://github.com/huskeee/godot-headless-mac/releases/download/${GODOT_VERSION_MAC}-stable/Godot_v${GODOT_VERSION_MAC}-stable_mac_headless.64.zip wget https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_macos.universal.zip
unzip -a Godot_v${GODOT_VERSION_MAC}-stable_mac_headless.64.zip unzip -a Godot_v${GODOT_VERSION}-stable_macos.universal.zip
- name: Setup 💻 - name: Setup 💻
run: mkdir -v -p ./build/mac run: mkdir -v -p ./build/mac
- name: Mac Build 🔧 - name: Mac Build 🔧
run: | run: |
chown runner ./bin/Godot chown runner ./Godot.app/Contents/MacOS/Godot
chmod +x ./bin/Godot chmod +x ./Godot.app/Contents/MacOS/Godot
./bin/Godot -v --export "Mac OSX" ./build/mac/Pixelorama.zip ./Godot.app/Contents/MacOS/Godot --headless -v --export-release "macOS" ./build/mac/Pixelorama.zip
- name: Make application executable 🔧 - name: Make application executable 🔧
run: | run: |
unzip -a ./build/mac/Pixelorama.zip -d ./build/mac unzip -a ./build/mac/Pixelorama.zip -d ./build/mac

View file

@ -9,7 +9,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
GODOT_VERSION: 3.5.2 GODOT_VERSION: 4.1.1
EXPORT_NAME: Pixelorama EXPORT_NAME: Pixelorama
jobs: jobs:
@ -17,7 +17,7 @@ jobs:
name: Web Export 🌐 name: Web Export 🌐
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: docker://barichello/godot-ci:3.5.2 image: docker://barichello/godot-ci:4.1.1
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -25,10 +25,10 @@ jobs:
submodules: true submodules: true
- name: Setup 💻 - name: Setup 💻
run: | run: |
mkdir -v -p build/web ~/.local/share/godot/templates mkdir -v -p build/web ~/.local/share/godot/export_templates
mv /root/.local/share/godot/templates/${GODOT_VERSION}.stable ~/.local/share/godot/templates/${GODOT_VERSION}.stable mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable
- name: Web Build 🔧 - name: Web Build 🔧
run: godot -v --export "HTML5" ./build/web/index.html run: godot --headless -v --export-release "Web" ./build/web/index.html
- name: Install rsync 📚 - name: Install rsync 📚
run: | run: |
apt-get update && apt-get install -y rsync apt-get update && apt-get install -y rsync

View file

@ -5,11 +5,9 @@ on:
branches: [ release ] branches: [ release ]
env: env:
GODOT_VERSION: 3.5.2 GODOT_VERSION: 4.1.1
GODOT_VERSION_MAC: 3.5.2
RASPBERRY_PI_BUILDS_VERSION: 1.15.0
EXPORT_NAME: Pixelorama EXPORT_NAME: Pixelorama
TAG: v0.11.2 TAG: v1.0
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
jobs: jobs:
@ -17,7 +15,7 @@ jobs:
name: Windows Export 🗔 name: Windows Export 🗔
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: docker://barichello/godot-ci:3.5.2 image: docker://barichello/godot-ci:4.1.1
steps: steps:
- name: Setup WINE, rcedit and NSIS 🍷 - name: Setup WINE, rcedit and NSIS 🍷
run: | run: |
@ -26,21 +24,21 @@ jobs:
wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe
mkdir -v -p ~/.local/share/rcedit mkdir -v -p ~/.local/share/rcedit
mv rcedit-x64.exe ~/.local/share/rcedit mv rcedit-x64.exe ~/.local/share/rcedit
godot -q godot --headless --quit
echo 'export/windows/wine = "/usr/bin/wine"' >> ~/.config/godot/editor_settings-3.tres echo 'export/windows/wine = "/usr/bin/wine"' >> ~/.config/godot/editor_settings-4.tres
echo 'export/windows/rcedit = "/github/home/.local/share/rcedit/rcedit-x64.exe"' >> ~/.config/godot/editor_settings-3.tres echo 'export/windows/rcedit = "/github/home/.local/share/rcedit/rcedit-x64.exe"' >> ~/.config/godot/editor_settings-4.tres
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Setup 💻 - name: Setup 💻
run: | run: |
mkdir -v -p build/windows-64bit build/windows-32bit ~/.local/share/godot/templates mkdir -v -p build/windows-64bit build/windows-32bit ~/.local/share/godot/export_templates
mv /root/.local/share/godot/templates/${GODOT_VERSION}.stable ~/.local/share/godot/templates/${GODOT_VERSION}.stable mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable
- name: Windows Build 🔧 - name: Windows Build 🔧
run: | run: |
godot -v --export "Windows Desktop 64-bit" ./build/windows-64bit/$EXPORT_NAME.exe godot --headless -v --export-release "Windows Desktop 64-bit" ./build/windows-64bit/$EXPORT_NAME.exe
godot -v --export "Windows Desktop 32-bit" ./build/windows-32bit/$EXPORT_NAME.exe godot --headless -v --export-release "Windows Desktop 32-bit" ./build/windows-32bit/$EXPORT_NAME.exe
- name: Copy pixelorama_data folder 📁 - name: Copy pixelorama_data folder 📁
run: | run: |
rm ./pixelorama_data/.gdignore rm ./pixelorama_data/.gdignore
@ -76,7 +74,7 @@ jobs:
name: Linux Export 🐧 name: Linux Export 🐧
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: docker://barichello/godot-ci:3.5.2 image: docker://barichello/godot-ci:4.1.1
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -84,39 +82,30 @@ jobs:
submodules: true submodules: true
- name: Setup 💻 - name: Setup 💻
run: | run: |
mkdir -v -p build/linux-64bit build/linux-32bit build/linux-rpi4 ~/.local/share/godot/templates mkdir -v -p build/linux-64bit build/linux-32bit ~/.local/share/godot/export_templates
mv /root/.local/share/godot/templates/${GODOT_VERSION}.stable ~/.local/share/godot/templates/${GODOT_VERSION}.stable mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable
- name: Download Unofficial Godot Raspberry Pi 4 Builds 🍇
run: |
wget https://github.com/hiulit/Unofficial-Godot-Engine-Raspberry-Pi/releases/download/v${RASPBERRY_PI_BUILDS_VERSION}/godot_${GODOT_VERSION}-stable_rpi4.zip
unzip -a godot_${GODOT_VERSION}-stable_rpi4.zip
- name: Linux Build 🔧 - name: Linux Build 🔧
run: | run: |
godot -v --export "Linux/X11 64-bit" ./build/linux-64bit/$EXPORT_NAME.x86_64 godot --headless -v --export-release "Linux/X11 64-bit" ./build/linux-64bit/$EXPORT_NAME.x86_64
godot -v --export "Linux/X11 32-bit" ./build/linux-32bit/$EXPORT_NAME.x86 godot --headless -v --export-release "Linux/X11 32-bit" ./build/linux-32bit/$EXPORT_NAME.x86
godot -v --export "Raspberry Pi 4" ./build/linux-rpi4/$EXPORT_NAME.rpi4
- name: Give execute permission ☑️ - name: Give execute permission ☑️
run: | run: |
chmod +x ./build/linux-64bit/$EXPORT_NAME.x86_64 chmod +x ./build/linux-64bit/$EXPORT_NAME.x86_64
chmod +x ./build/linux-32bit/$EXPORT_NAME.x86 chmod +x ./build/linux-32bit/$EXPORT_NAME.x86
chmod +x ./build/linux-rpi4/$EXPORT_NAME.rpi4
- name: Copy pixelorama_data folder 📁 - name: Copy pixelorama_data folder 📁
run: | run: |
rm ./pixelorama_data/.gdignore rm ./pixelorama_data/.gdignore
cp -R ./pixelorama_data ./build/linux-64bit cp -R ./pixelorama_data ./build/linux-64bit
cp -R ./pixelorama_data ./build/linux-32bit cp -R ./pixelorama_data ./build/linux-32bit
cp -R ./pixelorama_data ./build/linux-rpi4
- name: Create tar.gz archive 🗜️ - name: Create tar.gz archive 🗜️
run: | run: |
cd build cd build
tar zcvf Pixelorama.Linux-64bit.tar.gz linux-64bit tar zcvf Pixelorama.Linux-64bit.tar.gz linux-64bit
tar zcvf Pixelorama.Linux-32bit.tar.gz linux-32bit tar zcvf Pixelorama.Linux-32bit.tar.gz linux-32bit
tar zcvf Pixelorama.Linux-RPI4.tar.gz linux-rpi4
- name: Upload Release Assets to itch.io 🎮 - name: Upload Release Assets to itch.io 🎮
run: | run: |
butler push ./build/Pixelorama.Linux-64bit.tar.gz ${{ secrets.ITCHIO_USERNAME }}/${{ secrets.ITCHIO_GAME }}:linux-64 --userversion ${{env.TAG}} butler push ./build/Pixelorama.Linux-64bit.tar.gz ${{ secrets.ITCHIO_USERNAME }}/${{ secrets.ITCHIO_GAME }}:linux-64 --userversion ${{env.TAG}}
butler push ./build/Pixelorama.Linux-32bit.tar.gz ${{ secrets.ITCHIO_USERNAME }}/${{ secrets.ITCHIO_GAME }}:linux-32 --userversion ${{env.TAG}} butler push ./build/Pixelorama.Linux-32bit.tar.gz ${{ secrets.ITCHIO_USERNAME }}/${{ secrets.ITCHIO_GAME }}:linux-32 --userversion ${{env.TAG}}
butler push ./build/Pixelorama.Linux-RPI4.tar.gz ${{ secrets.ITCHIO_USERNAME }}/${{ secrets.ITCHIO_GAME }}:linux-rpi4 --userversion ${{env.TAG}}
- name: Upload Release Asset 🚀 - name: Upload Release Asset 🚀
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
with: with:
@ -136,25 +125,25 @@ jobs:
submodules: true submodules: true
- name: Setup environment 🔧 - name: Setup environment 🔧
run: | run: |
export GODOT_VERSION=${GODOT_VERSION_MAC} export GODOT_VERSION=${GODOT_VERSION}
export EXPORT_NAME=Pixelorama export EXPORT_NAME=Pixelorama
- name: Download and extract export templates 💾 - name: Download and extract export templates 💾
run: | run: |
mkdir -v -p ~/.local/share/godot/templates/${GODOT_VERSION_MAC}.stable mkdir -v -p "/Users/runner/Library/Application Support/Godot/export_templates/${GODOT_VERSION}.stable"
curl -O https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION_MAC}/Godot_v${GODOT_VERSION_MAC}-stable_export_templates.tpz curl -O https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION}/Godot_v${GODOT_VERSION}-stable_export_templates.tpz
unzip -a Godot_v${GODOT_VERSION_MAC}-stable_export_templates.tpz unzip -a Godot_v${GODOT_VERSION}-stable_export_templates.tpz
mv ./templates/* ~/.local/share/godot/templates/${GODOT_VERSION_MAC}.stable mv ./templates/* "/Users/runner/Library/Application Support/Godot/export_templates/${GODOT_VERSION}.stable"
- name: Download Godot headless binary 🤖 - name: Download Godot headless binary 🤖
run: | run: |
wget https://github.com/huskeee/godot-headless-mac/releases/download/${GODOT_VERSION_MAC}-stable/Godot_v${GODOT_VERSION_MAC}-stable_mac_headless.64.zip wget https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_macos.universal.zip
unzip -a Godot_v${GODOT_VERSION_MAC}-stable_mac_headless.64.zip unzip -a Godot_v${GODOT_VERSION}-stable_macos.universal.zip
- name: Setup 💻 - name: Setup 💻
run: mkdir -v -p ./build/mac run: mkdir -v -p ./build/mac
- name: Mac Build 🔧 - name: Mac Build 🔧
run: | run: |
chown runner ./bin/Godot chown runner ./Godot.app/Contents/MacOS/Godot
chmod +x ./bin/Godot chmod +x ./Godot.app/Contents/MacOS/Godot
./bin/Godot -v --export "Mac OSX" ./build/mac/Pixelorama.zip ./Godot.app/Contents/MacOS/Godot --headless -v --export-release "macOS" ./build/mac/Pixelorama.zip
- name: Make application executable 🔧 - name: Make application executable 🔧
run: | run: |
unzip -a ./build/mac/Pixelorama.zip -d ./build/mac unzip -a ./build/mac/Pixelorama.zip -d ./build/mac
@ -189,7 +178,7 @@ jobs:
name: Web Export 🌐 name: Web Export 🌐
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: docker://barichello/godot-ci:3.5.2 image: docker://barichello/godot-ci:4.1.1
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -200,7 +189,7 @@ jobs:
mkdir -v -p build/web ~/.local/share/godot/templates mkdir -v -p build/web ~/.local/share/godot/templates
mv /root/.local/share/godot/templates/${GODOT_VERSION}.stable ~/.local/share/godot/templates/${GODOT_VERSION}.stable mv /root/.local/share/godot/templates/${GODOT_VERSION}.stable ~/.local/share/godot/templates/${GODOT_VERSION}.stable
- name: Web Build 🔧 - name: Web Build 🔧
run: godot -v --export "HTML5" ./build/web/index.html run: godot --headless -v --export-release "Web" ./build/web/index.html
- name: Install rsync 📚 - name: Install rsync 📚
run: | run: |
apt-get update && apt-get install -y rsync apt-get update && apt-get install -y rsync

View file

@ -20,7 +20,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install gdtoolkit - name: Install gdtoolkit
run: pip3 install "gdtoolkit==3.*" run: pip3 install "gdtoolkit==4.*"
- name: Formatting checks - name: Formatting checks
run: gdformat --diff . run: gdformat --diff .
- name: Linting checks - name: Linting checks

View file

@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). All the dates are in YYYY-MM-DD format. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). All the dates are in YYYY-MM-DD format.
<br><br> <br><br>
## [v1.0] - Unreleased
Built using Godot 4.1.1
### Added
- Added some missing shortcuts for buttons. [#900](https://github.com/Orama-Interactive/Pixelorama/pull/900)
- The brush increment/decrement shortcuts can now be changed. [#900](https://github.com/Orama-Interactive/Pixelorama/pull/900)
- 3D layers now support torus shapes. [#900](https://github.com/Orama-Interactive/Pixelorama/pull/900)
- Image effect animation now supports the tweening transition method of spring. [#900](https://github.com/Orama-Interactive/Pixelorama/pull/900)
### Changed
- Every shader-based image effect is automatically working without the need to change renderers, and they all work now on the Web version. This comes at the cost of less compatibility, as the desktop version now requires OpenGL 3.3 minimum instead of 2.1, and the Web version requires WebGL 2 instead of WebGL 1. [#900](https://github.com/Orama-Interactive/Pixelorama/pull/900)
### Fixed
- Performance when drawing and doing operations such as bucket area fill should be better now. [#900](https://github.com/Orama-Interactive/Pixelorama/pull/900)
- Dividing by zero in value sliders and spinboxes no longer crashes the program.
## [v0.11.2] - 2023-08-31 ## [v0.11.2] - 2023-08-31
This update has been brought to you by the contributions of: This update has been brought to you by the contributions of:
[@Lsbt1](https://github.com/Lsbt1), Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind)) [@Lsbt1](https://github.com/Lsbt1), Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind))

View file

@ -61,7 +61,7 @@ You can find Online Documentation for Pixelorama here: https://orama-interactive
It's still work in progress so there are some pages missing. If you want to contribute, you can do so in [Pixelorama-Docs' GitHub Repository](https://github.com/Orama-Interactive/Pixelorama-Docs). It's still work in progress so there are some pages missing. If you want to contribute, you can do so in [Pixelorama-Docs' GitHub Repository](https://github.com/Orama-Interactive/Pixelorama-Docs).
## Cloning Instructions ## Cloning Instructions
Pixelorama uses Godot 3.5, so you will need to have it in order to run the project. Older versions may not work. Pixelorama uses Godot 4.1, so you will need to have it in order to run the project. Older versions will not work.
As of right now, most of the code is written using GDScript, so the mono version of Godot is not required, but Pixelorama should also work with it. As of right now, most of the code is written using GDScript, so the mono version of Godot is not required, but Pixelorama should also work with it.
## Current features: ## Current features:

View file

@ -1655,27 +1655,21 @@ msgid "Current frame as spritesheet"
msgstr "" msgstr ""
msgid "Jump to the first frame\n" msgid "Jump to the first frame\n"
"(%s)"
msgstr "" msgstr ""
msgid "Go to the previous frame\n" msgid "Go to the previous frame\n"
"(%s)"
msgstr "" msgstr ""
msgid "Play the animation backwards (from end to beginning)\n" msgid "Play the animation backwards (from end to beginning)\n"
"(%s)"
msgstr "" msgstr ""
msgid "Play the animation forward (from beginning to end)\n" msgid "Play the animation forward (from beginning to end)\n"
"(%s)"
msgstr "" msgstr ""
msgid "Go to the next frame\n" msgid "Go to the next frame\n"
"(%s)"
msgstr "" msgstr ""
msgid "Jump to the last frame\n" msgid "Jump to the last frame\n"
"(%s)"
msgstr "" msgstr ""
msgid "Onion Skinning settings" msgid "Onion Skinning settings"
@ -2614,6 +2608,10 @@ msgstr ""
msgid "Backing out at ends" msgid "Backing out at ends"
msgstr "" msgstr ""
#. Found under certain image effects that support properties that can be animated. A type of interpolation.
msgid "Spring towards the end"
msgstr ""
msgid "Silhouette" msgid "Silhouette"
msgstr "" msgstr ""

View file

@ -3,19 +3,19 @@
## aimgio ## aimgio
- Upstream: https://gitlab.com/20kdc/20kdc_godot_addons - Upstream: https://gitlab.com/20kdc/20kdc_godot_addons
- Version: Presently git commit b6a1411758c856ad543f4f661ca4105aa7c0ab6d - Version: ba00188e9da1ae229181f106788fcb72ccdd85fa
- License: [Unlicense](https://gitlab.com/20kdc/20kdc_godot_addons/-/blob/b6a1411758c856ad543f4f661ca4105aa7c0ab6d/addons/aimg_io/COPYING.txt) - License: [Unlicense](https://gitlab.com/20kdc/20kdc_godot_addons/-/blob/master/godot4/addons/aimg_io/COPYING.txt)
## Keychain ## Keychain
- Upstream: https://github.com/Orama-Interactive/Keychain - Upstream: https://github.com/Orama-Interactive/Keychain
- Version: Based on git commit 22539c2b9b9e781c0cd54f32d92c77b6f36837e0 with slight modifications in Keychain.gd and ShortcutEdit.tscn. - Version: 4a08d6b380929031b2c9202811ae0a65d850a5ac
- License: [MIT](https://github.com/Orama-Interactive/Keychain/blob/main/LICENSE) - License: [MIT](https://github.com/Orama-Interactive/Keychain/blob/main/LICENSE)
## gdgifexporter ## gdgifexporter
- Upstream: https://github.com/jegor377/godot-gdgifexporter - Upstream: https://github.com/jegor377/godot-gdgifexporter
- Version: custom - Version: c368952a97bbbbcfa491358a61043be7009536a6
- License: [MIT](https://github.com/jegor377/godot-gdgifexporter/blob/master/LICENSE) - License: [MIT](https://github.com/jegor377/godot-gdgifexporter/blob/master/LICENSE)
Files extracted from source: Files extracted from source:
@ -24,13 +24,13 @@ Files extracted from source:
## godot-dockable-container ## godot-dockable-container
- Upstream: https://github.com/gilzoide/godot-dockable-container - Upstream: https://github.com/gilzoide/godot-dockable-container
- Version: Based on git commit e5df60ed1d53246e03dba36053ff009846ba5174 with a modification on dockable_container.gd (lines 187-191). - Version: 9a93231d7f449aad039b28ff05bc1ef40a9956ce
- License: [CC0-1.0](https://github.com/gilzoide/godot-dockable-container/blob/main/LICENSE) - License: [CC0-1.0](https://github.com/gilzoide/godot-dockable-container/blob/main/LICENSE)
## SmartSlicer ## SmartSlicer
- Upstream: https://github.com/Variable-Interactive/SmartSlicer - Upstream: https://github.com/Variable-Interactive/SmartSlicer
- Version: Based on git commit 2804e6109f9667022c66522ce88a99a56fd67ca8 with a modification on SmartSlicePreview.gd (lines 31-32). Only the contents of addons folder are used and the script SmartSlicePreview.gd is moved to res://src/UI/Dialogs/HelperScripts/ for better organization - Version: Based on git commit 5d65d2ff556fed878099c96546ceb307aa3bc030 with a modification on SmartSlicePreview.gd (lines 31-32). Only the contents of addons folder are used and the script SmartSlicePreview.gd is moved to res://src/UI/Dialogs/HelperScripts/ for better organization
- License: [MIT](https://github.com/Variable-Interactive/SmartSlicer/blob/main/LICENSE) - License: [MIT](https://github.com/Variable-Interactive/SmartSlicer/blob/main/LICENSE)

View file

@ -1,19 +1,42 @@
class_name RegionUnpacker class_name RegionUnpacker
extends Reference extends RefCounted
# THIS CLASS TAKES INSPIRATION FROM PIXELORAMA'S FLOOD FILL # THIS CLASS TAKES INSPIRATION FROM PIXELORAMA'S FLOOD FILL
# AND HAS BEEN MODIFIED FOR OPTIMIZATION # AND HAS BEEN MODIFIED FOR OPTIMIZATION
var slice_thread := Thread.new() var slice_thread := Thread.new()
var _include_boundary_threshold: int # the size of rect below which merging accounts for boundaty var _include_boundary_threshold: int ## Τhe size of rect below which merging accounts for boundary
var _merge_dist: int # after crossing threshold the smaller image will merge with larger image ## After crossing threshold the smaller image will merge with larger image
# if it is within the _merge_dist ## if it is within the _merge_dist
var _merge_dist: int
# working array used as buffer for segments while flooding ## Working array used as buffer for segments while flooding
var _allegro_flood_segments: Array var _allegro_flood_segments: Array[Segment]
# results array per image while flooding ## Results array per image while flooding
var _allegro_image_segments: Array var _allegro_image_segments: Array[Segment]
class RectData:
var rects: Array[Rect2i]
var frame_size: Vector2i
func _init(_rects: Array[Rect2i], _frame_size: Vector2i):
rects = _rects
frame_size = _frame_size
class Segment:
var flooding := false
var todo_above := false
var todo_below := false
var left_position := -5
var right_position := -5
var y := 0
var next := 0
func _init(_y: int) -> void:
y = _y
func _init(threshold: int, merge_dist: int) -> void: func _init(threshold: int, merge_dist: int) -> void:
@ -21,63 +44,58 @@ func _init(threshold: int, merge_dist: int) -> void:
_merge_dist = merge_dist _merge_dist = merge_dist
func get_used_rects(image: Image) -> Dictionary: func get_used_rects(image: Image) -> RectData:
if OS.get_name() == "HTML5": if ProjectSettings.get_setting("rendering/driver/threads/thread_model") != 2:
# Single-threaded mode
return get_rects(image) return get_rects(image)
else: else: # Multi-threaded mode
# If Thread model is set to "Multi-Threaded" in project settings>threads>thread model if slice_thread.is_started():
if slice_thread.is_active():
slice_thread.wait_to_finish() slice_thread.wait_to_finish()
var error = slice_thread.start(self, "get_rects", image) var error := slice_thread.start(get_rects.bind(image))
if error == OK: if error == OK:
return slice_thread.wait_to_finish() return slice_thread.wait_to_finish()
else: else:
return get_rects(image) return get_rects(image)
# If Thread model is set to "Single-Safe" in project settings>threads>thread model then
# comment the above code and uncomment below
#return get_rects({"image": image})
func get_rects(image: Image) -> Dictionary: func get_rects(image: Image) -> RectData:
# make a smaller image to make the loop shorter # Make a smaller image to make the loop shorter
var used_rect = image.get_used_rect() var used_rect := image.get_used_rect()
if used_rect.size == Vector2.ZERO: if used_rect.size == Vector2i.ZERO:
return clean_rects([]) return clean_rects([])
var test_image = image.get_rect(used_rect) var test_image := image.get_region(used_rect)
# prepare a bitmap to keep track of previous places # Prepare a bitmap to keep track of previous places
var scanned_area := BitMap.new() var scanned_area := BitMap.new()
scanned_area.create(test_image.get_size()) scanned_area.create(test_image.get_size())
test_image.lock()
# Scan the image # Scan the image
var rects = [] var rects: Array[Rect2i] = []
var frame_size = Vector2.ZERO var frame_size := Vector2i.ZERO
for y in test_image.get_size().y: for y in test_image.get_size().y:
for x in test_image.get_size().x: for x in test_image.get_size().x:
var position = Vector2(x, y) var position := Vector2i(x, y)
if test_image.get_pixelv(position).a > 0: # used portion of image detected if test_image.get_pixelv(position).a > 0: # used portion of image detected
if !scanned_area.get_bit(position): if !scanned_area.get_bitv(position):
var rect := _estimate_rect(test_image, position) var rect := _estimate_rect(test_image, position)
scanned_area.set_bit_rect(rect, true) scanned_area.set_bit_rect(rect, true)
rect.position += used_rect.position rect.position += used_rect.position
rects.append(rect) rects.append(rect)
test_image.unlock() var rects_info := clean_rects(rects)
var rects_info = clean_rects(rects) rects_info.rects.sort_custom(sort_rects)
rects_info["rects"].sort_custom(self, "sort_rects")
return rects_info return rects_info
func clean_rects(rects: Array) -> Dictionary: func clean_rects(rects: Array[Rect2i]) -> RectData:
var frame_size = Vector2.ZERO var frame_size := Vector2i.ZERO
for i in rects.size(): for i in rects.size():
var target: Rect2 = rects.pop_front() var target: Rect2i = rects.pop_front()
var test_rect = target var test_rect := target
if ( if (
target.size.x < _include_boundary_threshold target.size.x < _include_boundary_threshold
or target.size.y < _include_boundary_threshold or target.size.y < _include_boundary_threshold
): ):
test_rect.size += Vector2(_merge_dist, _merge_dist) test_rect.size += Vector2i(_merge_dist, _merge_dist)
test_rect.position -= Vector2(_merge_dist, _merge_dist) / 2 test_rect.position -= Vector2i(_merge_dist, _merge_dist) / 2
var merged = false var merged := false
for rect_i in rects.size(): for rect_i in rects.size():
if test_rect.intersects(rects[rect_i]): if test_rect.intersects(rects[rect_i]):
rects[rect_i] = target.merge(rects[rect_i]) rects[rect_i] = target.merge(rects[rect_i])
@ -91,61 +109,51 @@ func clean_rects(rects: Array) -> Dictionary:
frame_size.x = target.size.x frame_size.x = target.size.x
if target.size.y > frame_size.y: if target.size.y > frame_size.y:
frame_size.y = target.size.y frame_size.y = target.size.y
return {"rects": rects, "frame_size": frame_size} return RectData.new(rects, frame_size)
func sort_rects(rect_a: Rect2, rect_b: Rect2) -> bool: func sort_rects(rect_a: Rect2i, rect_b: Rect2i) -> bool:
# After many failed attempts, this version works for some reason (it's best not to disturb it) # After many failed attempts, this version works for some reason (it's best not to disturb it)
if rect_a.end.y < rect_b.position.y: if rect_a.end.y < rect_b.position.y:
return true return true
if rect_a.position.x < rect_b.position.x: if rect_a.position.x < rect_b.position.x:
# if both lie in the same row # if both lie in the same row
var start = rect_a.position var start := rect_a.position
var size = Vector2(rect_b.end.x, rect_a.end.y) var size := Vector2i(rect_b.end.x, rect_a.end.y)
if Rect2(start, size).intersects(rect_b): if Rect2i(start, size).intersects(rect_b):
return true return true
return false return false
func _estimate_rect(image: Image, position: Vector2) -> Rect2: func _estimate_rect(image: Image, position: Vector2) -> Rect2i:
var cel_image := Image.new() var cel_image := Image.new()
cel_image.copy_from(image) cel_image.copy_from(image)
cel_image.lock() var small_rect := _flood_fill(position, cel_image)
var small_rect: Rect2 = _flood_fill(position, cel_image)
cel_image.unlock()
return small_rect return small_rect
# Add a new segment to the array ## Add a new segment to the array
func _add_new_segment(y: int = 0) -> void: func _add_new_segment(y := 0) -> void:
var segment = {} _allegro_flood_segments.append(Segment.new(y))
segment.flooding = false
segment.todo_above = false
segment.todo_below = false
segment.left_position = -5 # anything less than -1 is ok
segment.right_position = -5
segment.y = y
segment.next = 0
_allegro_flood_segments.append(segment)
# fill an horizontal segment around the specified position, and adds it to the ## Fill an horizontal segment around the specified position, and adds it to the
# list of segments filled. Returns the first x coordinate after the part of the ## list of segments filled. Returns the first x coordinate after the part of the
# line that has been filled. ## line that has been filled.
func _flood_line_around_point(position: Vector2, image: Image) -> int: ## this method is called by `_flood_fill` after the required data structures
# this method is called by `_flood_fill` after the required data structures ## have been initialized
# have been initialized func _flood_line_around_point(position: Vector2i, image: Image) -> int:
if not image.get_pixelv(position).a > 0: if not image.get_pixelv(position).a > 0:
return int(position.x) + 1 return position.x + 1
var west: Vector2 = position var west := position
var east: Vector2 = position var east := position
while west.x >= 0 && image.get_pixelv(west).a > 0: while west.x >= 0 && image.get_pixelv(west).a > 0:
west += Vector2.LEFT west += Vector2i.LEFT
while east.x < image.get_width() && image.get_pixelv(east).a > 0: while east.x < image.get_width() && image.get_pixelv(east).a > 0:
east += Vector2.RIGHT east += Vector2i.RIGHT
# Make a note of the stuff we processed # Make a note of the stuff we processed
var c = int(position.y) var c := position.y
var segment = _allegro_flood_segments[c] var segment := _allegro_flood_segments[c]
# we may have already processed some segments on this y coordinate # we may have already processed some segments on this y coordinate
if segment.flooding: if segment.flooding:
while segment.next > 0: while segment.next > 0:
@ -154,7 +162,7 @@ func _flood_line_around_point(position: Vector2, image: Image) -> int:
# found last current segment on this line # found last current segment on this line
c = _allegro_flood_segments.size() c = _allegro_flood_segments.size()
segment.next = c segment.next = c
_add_new_segment(int(position.y)) _add_new_segment(position.y)
segment = _allegro_flood_segments[c] segment = _allegro_flood_segments[c]
# set the values for the current segment # set the values for the current segment
segment.flooding = true segment.flooding = true
@ -177,28 +185,28 @@ func _flood_line_around_point(position: Vector2, image: Image) -> int:
_allegro_image_segments.append(segment) _allegro_image_segments.append(segment)
# we know the point just east of the segment is not part of a segment that should be # we know the point just east of the segment is not part of a segment that should be
# processed, else it would be part of this segment # processed, else it would be part of this segment
return int(east.x) + 1 return east.x + 1
func _check_flooded_segment(y: int, left: int, right: int, image: Image) -> bool: func _check_flooded_segment(y: int, left: int, right: int, image: Image) -> bool:
var ret = false var ret := false
var c: int = 0 var c := 0
while left <= right: while left <= right:
c = y c = y
while true: while true:
var segment = _allegro_flood_segments[c] var segment := _allegro_flood_segments[c]
if left >= segment.left_position and left <= segment.right_position: if left >= segment.left_position and left <= segment.right_position:
left = segment.right_position + 2 left = segment.right_position + 2
break break
c = segment.next c = segment.next
if c == 0: # couldn't find a valid segment, so we draw a new one if c == 0: # couldn't find a valid segment, so we draw a new one
left = _flood_line_around_point(Vector2(left, y), image) left = _flood_line_around_point(Vector2i(left, y), image)
ret = true ret = true
break break
return ret return ret
func _flood_fill(position: Vector2, image: Image) -> Rect2: func _flood_fill(position: Vector2i, image: Image) -> Rect2i:
# implements the floodfill routine by Shawn Hargreaves # implements the floodfill routine by Shawn Hargreaves
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
# init flood data structures # init flood data structures
@ -208,17 +216,15 @@ func _flood_fill(position: Vector2, image: Image) -> Rect2:
# now actually color the image: since we have already checked a few things for the points # now actually color the image: since we have already checked a few things for the points
# we'll process here, we're going to skip a bunch of safety checks to speed things up. # we'll process here, we're going to skip a bunch of safety checks to speed things up.
var final_image = Image.new() var final_image := Image.new()
final_image.copy_from(image) final_image.copy_from(image)
final_image.fill(Color.transparent) final_image.fill(Color.TRANSPARENT)
final_image.lock()
_select_segments(final_image) _select_segments(final_image)
final_image.unlock()
return final_image.get_used_rect() return final_image.get_used_rect()
func _compute_segments_for_image(position: Vector2, image: Image) -> void: func _compute_segments_for_image(position: Vector2i, image: Image) -> void:
# initially allocate at least 1 segment per line of image # initially allocate at least 1 segment per line of image
for j in image.get_height(): for j in image.get_height():
_add_new_segment(j) _add_new_segment(j)
@ -228,9 +234,9 @@ func _compute_segments_for_image(position: Vector2, image: Image) -> void:
var done := false var done := false
while not done: while not done:
done = true done = true
var max_index = _allegro_flood_segments.size() var max_index := _allegro_flood_segments.size()
for c in max_index: for c in max_index:
var p = _allegro_flood_segments[c] var p := _allegro_flood_segments[c]
if p.todo_below: # check below the segment? if p.todo_below: # check below the segment?
p.todo_below = false p.todo_below = false
if _check_flooded_segment(p.y + 1, p.left_position, p.right_position, image): if _check_flooded_segment(p.y + 1, p.left_position, p.right_position, image):
@ -244,8 +250,8 @@ func _compute_segments_for_image(position: Vector2, image: Image) -> void:
func _select_segments(map: Image) -> void: func _select_segments(map: Image) -> void:
# short circuit for flat colors # short circuit for flat colors
for c in _allegro_image_segments.size(): for c in _allegro_image_segments.size():
var p = _allegro_image_segments[c] var p := _allegro_image_segments[c]
var rect = Rect2() var rect := Rect2i()
rect.position = Vector2(p.left_position, p.y) rect.position = Vector2i(p.left_position, p.y)
rect.end = Vector2(p.right_position + 1, p.y + 1) rect.end = Vector2i(p.right_position + 1, p.y + 1)
map.fill_rect(rect, Color.white) map.fill_rect(rect, Color.WHITE)

View file

@ -13,7 +13,7 @@ func export_animation(
progress_report_obj: Object, progress_report_obj: Object,
progress_report_method, progress_report_method,
progress_report_args progress_report_args
) -> PoolByteArray: ) -> PackedByteArray:
var frame_count := len(frames) var frame_count := len(frames)
var result := AImgIOAPNGStream.new() var result := AImgIOAPNGStream.new()
# Magic number # Magic number
@ -65,7 +65,7 @@ func export_animation(
# setup chunk interior... # setup chunk interior...
var ichk := result.start_chunk() var ichk := result.start_chunk()
write_padded_lines(ichk, image) write_padded_lines(ichk, image)
chunk.put_data(ichk.data_array.compress(File.COMPRESSION_DEFLATE)) chunk.put_data(ichk.data_array.compress(FileAccess.COMPRESSION_DEFLATE))
# done with chunk interior # done with chunk interior
if i == 0: if i == 0:
result.write_chunk("IDAT", chunk.data_array) result.write_chunk("IDAT", chunk.data_array)
@ -74,7 +74,7 @@ func export_animation(
# Done with this frame! # Done with this frame!
progress_report_obj.callv(progress_report_method, progress_report_args) progress_report_obj.callv(progress_report_method, progress_report_args)
# Final chunk. # Final chunk.
result.write_chunk("IEND", PoolByteArray()) result.write_chunk("IEND", PackedByteArray())
return result.finish() return result.finish()
@ -88,7 +88,7 @@ func write_delay(sp: StreamPeer, duration: float, fps_hint: float):
# Precision is increased so we catch more complex cases. # Precision is increased so we catch more complex cases.
# But you should always get perfection for integers. # But you should always get perfection for integers.
var den := min(32767, max(fps_hint, 1)) var den := min(32767, max(fps_hint, 1))
var num := max(duration, 0) * den var num: float = max(duration, 0) * den
# If the FPS hint brings us out of range before we start, try some obvious integers # If the FPS hint brings us out of range before we start, try some obvious integers
var fallback := 10000 var fallback := 10000
while num > 32767: while num > 32767:
@ -123,7 +123,7 @@ func write_padded_lines(sp: StreamPeer, img: Image):
var base := 0 var base := 0
while y < h: while y < h:
var nl := base + (w * 4) var nl := base + (w * 4)
var line := data.subarray(base, nl - 1) var line := data.slice(base, nl)
sp.put_8(0) sp.put_8(0)
sp.put_data(line) sp.put_data(line)
y += 1 y += 1

View file

@ -1,80 +1,60 @@
tool @tool
class_name AImgIOAPNGImportPlugin class_name AImgIOAPNGImportPlugin
extends EditorImportPlugin extends EditorImportPlugin
func get_importer_name() -> String: func _get_importer_name() -> String:
return "aimgio.apng_animatedtexture" return "aimgio.apng_animatedtexture"
func get_visible_name() -> String: func _get_visible_name() -> String:
return "APNG as AnimatedTexture" return "APNG as AnimatedTexture"
func get_save_extension() -> String: func _get_save_extension() -> String:
return "res" return "res"
func get_resource_type() -> String: func _get_resource_type() -> String:
return "AnimatedTexture" return "AnimatedTexture"
func get_recognized_extensions() -> Array: func _get_recognized_extensions() -> PackedStringArray:
return ["png"] return PackedStringArray(["png"])
func get_preset_count(): func _get_preset_count():
return 1 return 1
func get_preset_name(_i): func _get_preset_name(_i):
return "Default" return "Default"
func get_import_options(_i): func _get_import_options(_path: String, _i: int) -> Array[Dictionary]:
# GDLint workaround - it really does not want this string to exist due to length. return []
var hint = "Mipmaps,Repeat,Filter,Anisotropic Filter,Convert To Linear,Mirrored Repeat"
return [
{
"name": "image_texture_storage",
"default_value": 2,
"property_hint": PROPERTY_HINT_ENUM_SUGGESTION,
"hint_string": "Raw,Lossy,Lossless"
},
{"name": "image_texture_lossy_quality", "default_value": 0.7},
{
"name": "texture_flags",
"default_value": 7,
"property_hint": PROPERTY_HINT_FLAGS,
"hint_string": hint
},
# We don't know if Godot will change things somehow.
{"name": "texture_flags_add", "default_value": 0}
]
func get_option_visibility(_option, _options): func _get_import_order():
return 0
func _get_option_visibility(_path: String, _option, _options):
return true return true
func import(load_path: String, save_path: String, options, _platform_variants, _gen_files): func _import(load_path: String, save_path: String, _options, _platform_variants, _gen_files):
var res := AImgIOAPNGImporter.load_from_file(load_path) var res := AImgIOAPNGImporter.load_from_file(load_path)
if res[0] != null: if res[0] != null:
push_error("AImgIOPNGImporter: " + res[0]) push_error("AImgIOPNGImporter: " + res[0])
return ERR_FILE_UNRECOGNIZED return ERR_FILE_UNRECOGNIZED
var frames: Array = res[1] var frames: Array = res[1]
var root: AnimatedTexture = AnimatedTexture.new() var root: AnimatedTexture = AnimatedTexture.new()
var flags: int = options["texture_flags"]
flags |= options["texture_flags_add"]
root.flags = flags
root.frames = len(frames) root.frames = len(frames)
root.fps = 1
for i in range(len(frames)): for i in range(len(frames)):
var f: AImgIOFrame = frames[i] var f: AImgIOFrame = frames[i]
root.set_frame_delay(i, f.duration - 1.0) root.set_frame_duration(i, f.duration)
var tx := ImageTexture.new() var tx := ImageTexture.new()
tx.storage = options["image_texture_storage"] tx.create_from_image(f.content)
tx.lossy_quality = options["image_texture_lossy_quality"]
tx.create_from_image(f.content, flags)
root.set_frame_texture(i, tx) root.set_frame_texture(i, tx)
return ResourceSaver.save(save_path + ".res", root) return ResourceSaver.save(root, save_path + ".res")

View file

@ -1,6 +1,6 @@
tool @tool
class_name AImgIOAPNGImporter class_name AImgIOAPNGImporter
extends Reference extends RefCounted
# Will NOT import regular, unanimated PNGs - use Image.load_png_from_buffer # Will NOT import regular, unanimated PNGs - use Image.load_png_from_buffer
# This is because we don't want to import the default image as a frame # This is because we don't want to import the default image as a frame
# Therefore it just uses the rule: # Therefore it just uses the rule:
@ -10,7 +10,7 @@ extends Reference
# Imports an APNG PoolByteArray into an animation as an Array of frames. # Imports an APNG PoolByteArray into an animation as an Array of frames.
# Returns [error, frames] similar to some read functions. # Returns [error, frames] similar to some read functions.
# However, error is a string. # However, error is a string.
static func load_from_buffer(buffer: PoolByteArray) -> Array: static func load_from_buffer(buffer: PackedByteArray) -> Array:
var stream := AImgIOAPNGStream.new(buffer) var stream := AImgIOAPNGStream.new(buffer)
var magic_str = stream.read_magic() var magic_str = stream.read_magic()
if magic_str != null: if magic_str != null:
@ -35,14 +35,14 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
# So to convert an APNG frame to a PNG for reading, we need to stitch: # So to convert an APNG frame to a PNG for reading, we need to stitch:
# IHDR (modified), PLTE (if present), tRNS (if present), IDAT (from fdAT), # IHDR (modified), PLTE (if present), tRNS (if present), IDAT (from fdAT),
# and IEND (generated). # and IEND (generated).
var ihdr := PoolByteArray() var ihdr := PackedByteArray()
var plte := PoolByteArray() var plte := PackedByteArray()
var trns := PoolByteArray() var trns := PackedByteArray()
# stored full width/height for buffer # stored full width/height for buffer
var width := 0 var width := 0
var height := 0 var height := 0
# parse chunks # parse chunks
var frames := [] var frames: Array[BFrame] = []
while stream.read_chunk() == OK: while stream.read_chunk() == OK:
if stream.chunk_type == "IHDR": if stream.chunk_type == "IHDR":
ihdr = stream.chunk_data ihdr = stream.chunk_data
@ -78,17 +78,16 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
if len(frames) > 0: if len(frames) > 0:
var f: BFrame = frames[len(frames) - 1] var f: BFrame = frames[len(frames) - 1]
if len(stream.chunk_data) >= 4: if len(stream.chunk_data) >= 4:
var data := stream.chunk_data.subarray(4, len(stream.chunk_data) - 1) var data := stream.chunk_data.slice(4, len(stream.chunk_data))
f.add_data(data) f.add_data(data)
# theoretically we *could* store the default frame somewhere, but *why*? # theoretically we *could* store the default frame somewhere, but *why*?
# just use Image functions if you want that # just use Image functions if you want that
if len(frames) == 0: if len(frames) == 0:
return ["No frames", null] return ["No frames", null]
# prepare initial operating buffer # prepare initial operating buffer
var operating := Image.new() var operating := Image.create(width, height, false, Image.FORMAT_RGBA8)
operating.create(width, height, false, Image.FORMAT_RGBA8)
operating.fill(Color(0, 0, 0, 0)) operating.fill(Color(0, 0, 0, 0))
var finished := [] var finished: Array[AImgIOFrame] = []
for v in frames: for v in frames:
var fv: BFrame = v var fv: BFrame = v
# Ok, so to avoid having to deal with filters and stuff, # Ok, so to avoid having to deal with filters and stuff,
@ -106,9 +105,9 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
var blit_target := operating var blit_target := operating
var copy_blit_target := true var copy_blit_target := true
# rectangles and such # rectangles and such
var blit_src := Rect2(Vector2.ZERO, intermediary_img.get_size()) var blit_src := Rect2i(Vector2i.ZERO, intermediary_img.get_size())
var blit_pos := Vector2(fv.x, fv.y) var blit_pos := Vector2i(fv.x, fv.y)
var blit_tgt := Rect2(blit_pos, intermediary_img.get_size()) var blit_tgt := Rect2i(blit_pos, intermediary_img.get_size())
# early dispose ops # early dispose ops
if fv.dispose_op == 2: if fv.dispose_op == 2:
# previous # previous
@ -118,10 +117,11 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
blit_target.copy_from(operating) blit_target.copy_from(operating)
copy_blit_target = false copy_blit_target = false
# actually blit # actually blit
if fv.blend_op == 0: if blit_src.size != Vector2i.ZERO:
blit_target.blit_rect(intermediary_img, blit_src, blit_pos) if fv.blend_op == 0:
else: blit_target.blit_rect(intermediary_img, blit_src, blit_pos)
blit_target.blend_rect(intermediary_img, blit_src, blit_pos) else:
blit_target.blend_rect(intermediary_img, blit_src, blit_pos)
# insert as frame # insert as frame
var ffin := AImgIOFrame.new() var ffin := AImgIOFrame.new()
ffin.duration = fv.duration ffin.duration = fv.duration
@ -143,10 +143,10 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
# Imports an APNG file into an animation as an array of frames. # Imports an APNG file into an animation as an array of frames.
# Returns null on error. # Returns null on error.
static func load_from_file(path: String) -> Array: static func load_from_file(path: String) -> Array:
var o := File.new() var o := FileAccess.open(path, FileAccess.READ)
if o.open(path, File.READ) != OK: if o == null:
return [null, "Unable to open file: " + path] return [null, "Unable to open file: " + path]
var l = o.get_len() var l = o.get_length()
var data = o.get_buffer(l) var data = o.get_buffer(l)
o.close() o.close()
return load_from_buffer(data) return load_from_buffer(data)
@ -154,7 +154,7 @@ static func load_from_file(path: String) -> Array:
# Intermediate frame structure # Intermediate frame structure
class BFrame: class BFrame:
extends Reference extends RefCounted
var dispose_op: int var dispose_op: int
var blend_op: int var blend_op: int
var x: int var x: int
@ -162,9 +162,9 @@ class BFrame:
var w: int var w: int
var h: int var h: int
var duration: float var duration: float
var data: PoolByteArray var data: PackedByteArray
func setup(fctl: PoolByteArray): func setup(fctl: PackedByteArray):
if len(fctl) < 26: if len(fctl) < 26:
return "" return ""
var sp := StreamPeerBuffer.new() var sp := StreamPeerBuffer.new()
@ -191,8 +191,8 @@ class BFrame:
# This can be loaded by Godot directly. # This can be loaded by Godot directly.
# This basically skips most of the APNG decoding process. # This basically skips most of the APNG decoding process.
func intermediary( func intermediary(
ihdr: PoolByteArray, plte: PoolByteArray, trns: PoolByteArray ihdr: PackedByteArray, plte: PackedByteArray, trns: PackedByteArray
) -> PoolByteArray: ) -> PackedByteArray:
# Might be important to note this operates on a copy of ihdr (by-value). # Might be important to note this operates on a copy of ihdr (by-value).
var sp := StreamPeerBuffer.new() var sp := StreamPeerBuffer.new()
sp.data_array = ihdr sp.data_array = ihdr
@ -207,8 +207,8 @@ class BFrame:
if len(trns) > 0: if len(trns) > 0:
intermed.write_chunk("tRNS", trns) intermed.write_chunk("tRNS", trns)
intermed.write_chunk("IDAT", data) intermed.write_chunk("IDAT", data)
intermed.write_chunk("IEND", PoolByteArray()) intermed.write_chunk("IEND", PackedByteArray())
return intermed.finish() return intermed.finish()
func add_data(d: PoolByteArray): func add_data(d: PackedByteArray):
data.append_array(d) data.append_array(d)

View file

@ -1,6 +1,6 @@
tool @tool
class_name AImgIOAPNGStream class_name AImgIOAPNGStream
extends Reference extends RefCounted
# APNG IO context. To be clear, this is still effectively magic. # APNG IO context. To be clear, this is still effectively magic.
# Quite critical we preload this. Preloading creates static variables. # Quite critical we preload this. Preloading creates static variables.
@ -8,7 +8,7 @@ extends Reference
var crc32: AImgIOCRC32 = preload("apng_crc32.tres") var crc32: AImgIOCRC32 = preload("apng_crc32.tres")
var chunk_type: String var chunk_type: String
var chunk_data: PoolByteArray var chunk_data: PackedByteArray
# The reason this must be a StreamPeerBuffer is simple: # The reason this must be a StreamPeerBuffer is simple:
# 1. We need to support in-memory IO for HTML5 to really work # 1. We need to support in-memory IO for HTML5 to really work
@ -21,7 +21,7 @@ var chunk_data: PoolByteArray
var _target: StreamPeerBuffer var _target: StreamPeerBuffer
func _init(t: PoolByteArray = PoolByteArray()): func _init(t: PackedByteArray = PackedByteArray()):
crc32.ensure_ready() crc32.ensure_ready()
_target = StreamPeerBuffer.new() _target = StreamPeerBuffer.new()
_target.big_endian = true _target.big_endian = true
@ -80,9 +80,9 @@ func start_chunk() -> StreamPeerBuffer:
# Writes a PNG chunk. # Writes a PNG chunk.
func write_chunk(type: String, data: PoolByteArray): func write_chunk(type: String, data: PackedByteArray):
_target.put_32(len(data)) _target.put_32(len(data))
var at := type.to_ascii() var at := type.to_ascii_buffer()
_target.put_data(at) _target.put_data(at)
_target.put_data(data) _target.put_data(data)
var crc := crc32.update(crc32.mask, at) var crc := crc32.update(crc32.mask, at)
@ -91,5 +91,5 @@ func write_chunk(type: String, data: PoolByteArray):
# Returns the data_array of the stream (to be used when you're done writing the file) # Returns the data_array of the stream (to be used when you're done writing the file)
func finish() -> PoolByteArray: func finish() -> PackedByteArray:
return _target.data_array return _target.data_array

View file

@ -1,5 +1,5 @@
class_name AImgIOBaseExporter class_name AImgIOBaseExporter
extends Reference extends RefCounted
# Represents a method for exporting animations. # Represents a method for exporting animations.
var mime_type: String var mime_type: String
@ -17,5 +17,5 @@ func export_animation(
_progress_report_obj: Object, _progress_report_obj: Object,
_progress_report_method, _progress_report_method,
_progress_report_args _progress_report_args
) -> PoolByteArray: ) -> PackedByteArray:
return PoolByteArray() return PackedByteArray()

View file

@ -1,4 +1,4 @@
tool @tool
class_name AImgIOCRC32 class_name AImgIOCRC32
extends Resource extends Resource
# CRC32 implementation that uses a Resource for better caching # CRC32 implementation that uses a Resource for better caching
@ -6,10 +6,10 @@ extends Resource
const INIT = 0xFFFFFFFF const INIT = 0xFFFFFFFF
# The reversed polynomial. # The reversed polynomial.
export var reversed_polynomial: int = 0xEDB88320 @export var reversed_polynomial: int = 0xEDB88320
# The mask (and initialization value). # The mask (and initialization value).
export var mask: int = 0xFFFFFFFF @export var mask: int = 0xFFFFFFFF
var crc32_table = [] var crc32_table = []
var _table_init_mutex: Mutex = Mutex.new() var _table_init_mutex: Mutex = Mutex.new()
@ -38,7 +38,7 @@ func ensure_ready():
# Performs the update step of CRC32 over some bytes. # Performs the update step of CRC32 over some bytes.
# Note that this is not the whole story. # Note that this is not the whole story.
# The CRC must be initialized to 0xFFFFFFFF, then updated, then bitwise-inverted. # The CRC must be initialized to 0xFFFFFFFF, then updated, then bitwise-inverted.
func update(crc: int, data: PoolByteArray) -> int: func update(crc: int, data: PackedByteArray) -> int:
var i := 0 var i := 0
var l := len(data) var l := len(data)
while i < l: while i < l:

View file

@ -1,4 +1,4 @@
tool @tool
extends EditorPlugin extends EditorPlugin
var apng_importer var apng_importer

View file

@ -1,5 +1,6 @@
@tool
class_name AImgIOFrame class_name AImgIOFrame
extends Reference extends RefCounted
# Represents a variable-timed frame of an animation. # Represents a variable-timed frame of an animation.
# Typically stuffed into an array. # Typically stuffed into an array.

View file

@ -1,42 +1,69 @@
tool @tool
class_name DockableContainer
extends Container extends Container
const SplitHandle = preload("split_handle.gd") const SplitHandle := preload("split_handle.gd")
const DockablePanel = preload("dockable_panel.gd") const DockablePanel := preload("dockable_panel.gd")
const ReferenceControl = preload("dockable_panel_reference_control.gd") const DragNDropPanel := preload("drag_n_drop_panel.gd")
const DragNDropPanel = preload("drag_n_drop_panel.gd")
const Layout = preload("layout.gd")
# gdlint: ignore=max-line-length @export var tab_alignment := TabBar.ALIGNMENT_CENTER:
export(int, "Left", "Center", "Right") var tab_align = TabContainer.ALIGN_CENTER setget set_tab_align, get_tab_align get:
export(bool) var tabs_visible := true setget set_tabs_visible, get_tabs_visible return _tab_align
# gdlint: ignore=max-line-length set(value):
export(bool) var use_hidden_tabs_for_min_size: bool setget set_use_hidden_tabs_for_min_size, get_use_hidden_tabs_for_min_size _tab_align = value
export(int) var rearrange_group = 0 for i in range(1, _panel_container.get_child_count()):
export(Resource) var layout = Layout.new() setget set_layout, get_layout var panel := _panel_container.get_child(i) as DockablePanel
# If `clone_layout_on_ready` is true, `layout` will be cloned on `_ready`. panel.tab_alignment = value
# This is useful for leaving layout Resources untouched in case you want to @export var use_hidden_tabs_for_min_size := false:
# restore layout to its default later. get:
export(bool) var clone_layout_on_ready = true return _use_hidden_tabs_for_min_size
set(value):
_use_hidden_tabs_for_min_size = value
for i in range(1, _panel_container.get_child_count()):
var panel := _panel_container.get_child(i) as DockablePanel
panel.use_hidden_tabs_for_min_size = value
@export var tabs_visible := true:
get:
return _tabs_visible
set(value):
_tabs_visible = value
for i in range(1, _panel_container.get_child_count()):
var panel := _panel_container.get_child(i) as DockablePanel
panel.tabs_visible = value
@export var rearrange_group := 0
@export var layout := DockableLayout.new():
get:
return _layout
set(value):
set_layout(value)
## If `clone_layout_on_ready` is true, `layout` will be cloned checked `_ready`.
## This is useful for leaving layout Resources untouched in case you want to
## restore layout to its default later.
@export var clone_layout_on_ready := true
var _layout = Layout.new() var _layout := DockableLayout.new()
var _panel_container = Container.new() var _panel_container := Container.new()
var _split_container = Container.new() var _split_container := Container.new()
var _drag_n_drop_panel = DragNDropPanel.new() var _drag_n_drop_panel := DragNDropPanel.new()
var _drag_panel: DockablePanel var _drag_panel: DockablePanel
var _tab_align = TabContainer.ALIGN_CENTER var _tab_align := TabBar.ALIGNMENT_CENTER
var _tabs_visible = true var _tabs_visible := true
var _use_hidden_tabs_for_min_size = false var _use_hidden_tabs_for_min_size := false
var _current_panel_index = 0 var _current_panel_index := 0
var _current_split_index = 0 var _current_split_index := 0
var _children_names = {} var _children_names := {}
var _layout_dirty = false var _layout_dirty := false
func _init() -> void:
child_entered_tree.connect(_child_entered_tree)
child_exiting_tree.connect(_child_exiting_tree)
func _ready() -> void: func _ready() -> void:
set_process_input(false) set_process_input(false)
_panel_container.name = "_panel_container" _panel_container.name = "_panel_container"
.add_child(_panel_container) add_child(_panel_container)
move_child(_panel_container, 0) move_child(_panel_container, 0)
_split_container.name = "_split_container" _split_container.name = "_split_container"
_split_container.mouse_filter = MOUSE_FILTER_PASS _split_container.mouse_filter = MOUSE_FILTER_PASS
@ -44,13 +71,12 @@ func _ready() -> void:
_drag_n_drop_panel.name = "_drag_n_drop_panel" _drag_n_drop_panel.name = "_drag_n_drop_panel"
_drag_n_drop_panel.mouse_filter = MOUSE_FILTER_PASS _drag_n_drop_panel.mouse_filter = MOUSE_FILTER_PASS
_drag_n_drop_panel.set_drag_forwarding(self)
_drag_n_drop_panel.visible = false _drag_n_drop_panel.visible = false
.add_child(_drag_n_drop_panel) add_child(_drag_n_drop_panel)
if not _layout: if not _layout:
set_layout(null) set_layout(null)
elif clone_layout_on_ready and not Engine.editor_hint: elif clone_layout_on_ready and not Engine.is_editor_hint():
set_layout(_layout.clone()) set_layout(_layout.clone())
@ -61,7 +87,7 @@ func _notification(what: int) -> void:
what == NOTIFICATION_DRAG_BEGIN what == NOTIFICATION_DRAG_BEGIN
and _can_handle_drag_data(get_viewport().gui_get_drag_data()) and _can_handle_drag_data(get_viewport().gui_get_drag_data())
): ):
_drag_n_drop_panel.set_enabled(true, not _layout.root.empty()) _drag_n_drop_panel.set_enabled(true, not _layout.root.is_empty())
set_process_input(true) set_process_input(true)
elif what == NOTIFICATION_DRAG_END: elif what == NOTIFICATION_DRAG_END:
_drag_n_drop_panel.set_enabled(false) _drag_n_drop_panel.set_enabled(false)
@ -71,10 +97,10 @@ func _notification(what: int) -> void:
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
assert(get_viewport().gui_is_dragging(), "FIXME: should only be called when dragging") assert(get_viewport().gui_is_dragging(), "FIXME: should only be called when dragging")
if event is InputEventMouseMotion: if event is InputEventMouseMotion:
var local_position = get_local_mouse_position() var local_position := get_local_mouse_position()
var panel var panel: DockablePanel
for i in range(1, _panel_container.get_child_count()): for i in range(1, _panel_container.get_child_count()):
var p = _panel_container.get_child(i) var p := _panel_container.get_child(i) as DockablePanel
if p.get_rect().has_point(local_position): if p.get_rect().has_point(local_position):
panel = p panel = p
break break
@ -84,43 +110,37 @@ func _input(event: InputEvent) -> void:
fit_child_in_rect(_drag_n_drop_panel, panel.get_child_rect()) fit_child_in_rect(_drag_n_drop_panel, panel.get_child_rect())
func add_child(node: Node, legible_unique_name: bool = false) -> void: func _child_entered_tree(node: Node) -> void:
.add_child(node, legible_unique_name) if node == _panel_container or node == _drag_n_drop_panel:
_drag_n_drop_panel.raise() return
_drag_n_drop_panel.move_to_front()
_track_and_add_node(node) _track_and_add_node(node)
func add_child_below_node(node: Node, child_node: Node, legible_unique_name: bool = false) -> void: func _child_exiting_tree(node: Node) -> void:
.add_child_below_node(node, child_node, legible_unique_name) if node == _panel_container or node == _drag_n_drop_panel:
_drag_n_drop_panel.raise() return
_track_and_add_node(child_node)
func remove_child(node: Node) -> void:
.remove_child(node)
_untrack_node(node) _untrack_node(node)
func can_drop_data_fw(_position: Vector2, data, from_control) -> bool: func _can_drop_data(_position: Vector2, data) -> bool:
return from_control == _drag_n_drop_panel and _can_handle_drag_data(data) return _can_handle_drag_data(data)
func drop_data_fw(_position: Vector2, data, from_control) -> void: func _drop_data(_position: Vector2, data) -> void:
assert(from_control == _drag_n_drop_panel, "FIXME") var from_node := get_node(data.from_path) as DockablePanel
var from_node: TabContainer = get_node(data.from_path)
if from_node == _drag_panel and _drag_panel.get_child_count() == 1: if from_node == _drag_panel and _drag_panel.get_child_count() == 1:
return return
var moved_tab = from_node.get_tab_control(data.tabc_element) var moved_tab := from_node.get_tab_control(data.tabc_element)
if moved_tab is ReferenceControl: if moved_tab is DockableReferenceControl:
moved_tab = moved_tab.reference_to moved_tab = moved_tab.reference_to
if not _is_managed_node(moved_tab): if not _is_managed_node(moved_tab):
moved_tab.get_parent().remove_child(moved_tab) moved_tab.get_parent().remove_child(moved_tab)
add_child(moved_tab) add_child(moved_tab)
if _drag_panel != null: if _drag_panel != null:
var margin = _drag_n_drop_panel.get_hover_margin() var margin := _drag_n_drop_panel.get_hover_margin()
_layout.split_leaf_with_node(_drag_panel.leaf, moved_tab, margin) _layout.split_leaf_with_node(_drag_panel.leaf, moved_tab, margin)
_layout_dirty = true _layout_dirty = true
@ -135,66 +155,47 @@ func set_control_as_current_tab(control: Control) -> void:
if is_control_hidden(control): if is_control_hidden(control):
push_warning("Trying to focus a hidden control") push_warning("Trying to focus a hidden control")
return return
var leaf = _layout.get_leaf_for_node(control) var leaf := _layout.get_leaf_for_node(control)
if not leaf: if not leaf:
return return
var position_in_leaf = leaf.find_node(control) var position_in_leaf := leaf.find_child(control)
if position_in_leaf < 0: if position_in_leaf < 0:
return return
var panel var panel: DockablePanel
for i in range(1, _panel_container.get_child_count()): for i in range(1, _panel_container.get_child_count()):
var p = _panel_container.get_child(i) var p := _panel_container.get_child(i) as DockablePanel
if p.leaf == leaf: if p.leaf == leaf:
panel = p panel = p
break break
if not panel: if not panel:
return return
panel.current_tab = clamp(position_in_leaf, 0, panel.get_tab_count() - 1) panel.current_tab = clampi(position_in_leaf, 0, panel.get_tab_count() - 1)
func set_layout(value: Layout) -> void: func set_layout(value: DockableLayout) -> void:
if value == null: if value == null:
value = Layout.new() value = DockableLayout.new()
if value == _layout: if value == _layout:
return return
if _layout and _layout.is_connected("changed", self, "queue_sort"): if _layout and _layout.changed.is_connected(queue_sort):
_layout.disconnect("changed", self, "queue_sort") _layout.changed.disconnect(queue_sort)
_layout = value _layout = value
_layout.connect("changed", self, "queue_sort") _layout.changed.connect(queue_sort)
_layout_dirty = true _layout_dirty = true
queue_sort() queue_sort()
func get_layout() -> Layout: func set_tab_alignment(value: TabBar.AlignmentMode) -> void:
return _layout
func set_tab_align(value: int) -> void:
_tab_align = value _tab_align = value
for i in range(1, _panel_container.get_child_count()): for i in range(1, _panel_container.get_child_count()):
var panel = _panel_container.get_child(i) var panel = _panel_container.get_child(i)
panel.tab_align = value panel.tab_alignment = value
func get_tab_align() -> int: func get_tab_align() -> int:
return _tab_align return _tab_align
func set_tabs_visible(value: bool) -> void:
_tabs_visible = value
for i in range(1, _panel_container.get_child_count()):
var panel = _panel_container.get_child(i)
if panel.get_tab_count() >= 2:
panel.tabs_visible = true
else:
panel.tabs_visible = value
queue_sort()
func get_tabs_visible() -> bool:
return _tabs_visible
func set_use_hidden_tabs_for_min_size(value: bool) -> void: func set_use_hidden_tabs_for_min_size(value: bool) -> void:
_use_hidden_tabs_for_min_size = value _use_hidden_tabs_for_min_size = value
for i in range(1, _panel_container.get_child_count()): for i in range(1, _panel_container.get_child_count()):
@ -206,35 +207,35 @@ func get_use_hidden_tabs_for_min_size() -> bool:
return _use_hidden_tabs_for_min_size return _use_hidden_tabs_for_min_size
func set_control_hidden(child: Control, hidden: bool) -> void: func set_control_hidden(child: Control, is_hidden: bool) -> void:
_layout.set_node_hidden(child, hidden) _layout.set_node_hidden(child, is_hidden)
func is_control_hidden(child: Control) -> bool: func is_control_hidden(child: Control) -> bool:
return _layout.is_node_hidden(child) return _layout.is_node_hidden(child)
func get_tabs() -> Array: func get_tabs() -> Array[Control]:
var tabs = [] var tabs: Array[Control] = []
for i in get_child_count(): for i in get_child_count():
var child = get_child(i) var child := get_child(i)
if _is_managed_node(child): if _is_managed_node(child):
tabs.append(child) tabs.append(child)
return tabs return tabs
func get_tab_count() -> int: func get_tab_count() -> int:
var count = 0 var count := 0
for i in get_child_count(): for i in get_child_count():
var child = get_child(i) var child := get_child(i)
if _is_managed_node(child): if _is_managed_node(child):
count += 1 count += 1
return count return count
func _can_handle_drag_data(data): func _can_handle_drag_data(data) -> bool:
if data is Dictionary and data.get("type") == "tabc_element": if data is Dictionary and data.get("type") == "tabc_element":
var tabc = get_node_or_null(data.get("from_path")) var tabc := get_node_or_null(data.get("from_path"))
return ( return (
tabc tabc
and tabc.has_method("get_tabs_rearrange_group") and tabc.has_method("get_tabs_rearrange_group")
@ -249,15 +250,15 @@ func _is_managed_node(node: Node) -> bool:
and node != _panel_container and node != _panel_container
and node != _drag_n_drop_panel and node != _drag_n_drop_panel
and node is Control and node is Control
and not node.is_set_as_toplevel() and not node.top_level
) )
func _update_layout_with_children() -> void: func _update_layout_with_children() -> void:
var names = PoolStringArray() var names := PackedStringArray()
_children_names.clear() _children_names.clear()
for i in range(1, get_child_count() - 1): for i in range(1, get_child_count() - 1):
var c = get_child(i) var c := get_child(i)
if _track_node(c): if _track_node(c):
names.append(c.name) names.append(c.name)
_layout.update_nodes(names) _layout.update_nodes(names)
@ -269,10 +270,10 @@ func _track_node(node: Node) -> bool:
return false return false
_children_names[node] = node.name _children_names[node] = node.name
_children_names[node.name] = node _children_names[node.name] = node
if not node.is_connected("renamed", self, "_on_child_renamed"): if not node.renamed.is_connected(_on_child_renamed):
node.connect("renamed", self, "_on_child_renamed", [node]) node.renamed.connect(_on_child_renamed.bind(node))
if not node.is_connected("tree_exiting", self, "_untrack_node"): if not node.tree_exiting.is_connected(_untrack_node):
node.connect("tree_exiting", self, "_untrack_node", [node]) node.tree_exiting.connect(_untrack_node.bind(node))
return true return true
@ -288,31 +289,31 @@ func _track_and_add_node(node: Node) -> void:
func _untrack_node(node: Node) -> void: func _untrack_node(node: Node) -> void:
_children_names.erase(node) _children_names.erase(node)
_children_names.erase(node.name) _children_names.erase(node.name)
if node.is_connected("renamed", self, "_on_child_renamed"): if node.renamed.is_connected(_on_child_renamed):
node.disconnect("renamed", self, "_on_child_renamed") node.renamed.disconnect(_on_child_renamed)
if node.is_connected("tree_exiting", self, "_untrack_node"): if node.tree_exiting.is_connected(_untrack_node):
node.disconnect("tree_exiting", self, "_untrack_node") node.tree_exiting.disconnect(_untrack_node)
_layout_dirty = true _layout_dirty = true
func _resort() -> void: func _resort() -> void:
assert(_panel_container, "FIXME: resorting without _panel_container") assert(_panel_container, "FIXME: resorting without _panel_container")
if _panel_container.get_position_in_parent() != 0: if _panel_container.get_index() != 0:
move_child(_panel_container, 0) move_child(_panel_container, 0)
if _drag_n_drop_panel.get_position_in_parent() < get_child_count() - 1: if _drag_n_drop_panel.get_index() < get_child_count() - 1:
_drag_n_drop_panel.raise() _drag_n_drop_panel.move_to_front()
if _layout_dirty: if _layout_dirty:
_update_layout_with_children() _update_layout_with_children()
var rect = Rect2(Vector2.ZERO, rect_size) var rect := Rect2(Vector2.ZERO, size)
fit_child_in_rect(_panel_container, rect) fit_child_in_rect(_panel_container, rect)
_panel_container.fit_child_in_rect(_split_container, rect) _panel_container.fit_child_in_rect(_split_container, rect)
_current_panel_index = 1 _current_panel_index = 1
_current_split_index = 0 _current_split_index = 0
var children_list = [] var children_list := []
_calculate_panel_and_split_list(children_list, _layout.root) _calculate_panel_and_split_list(children_list, _layout.root)
_fit_panel_and_split_list_to_rect(children_list, rect) _fit_panel_and_split_list_to_rect(children_list, rect)
@ -320,17 +321,17 @@ func _resort() -> void:
_untrack_children_after(_split_container, _current_split_index) _untrack_children_after(_split_container, _current_split_index)
# Calculate DockablePanel and SplitHandle minimum sizes, skipping empty ## Calculate DockablePanel and SplitHandle minimum sizes, skipping empty
# branches. ## branches.
# ##
# Returns a DockablePanel on non-empty leaves, a SplitHandle on non-empty ## Returns a DockablePanel checked non-empty leaves, a SplitHandle checked non-empty
# splits, `null` if the whole branch is empty and no space should be used. ## splits, `null` if the whole branch is empty and no space should be used.
# ##
# `result` will be filled with the non-empty nodes in this post-order tree ## `result` will be filled with the non-empty nodes in this post-order tree
# traversal. ## traversal.
func _calculate_panel_and_split_list(result: Array, layout_node: Layout.LayoutNode): func _calculate_panel_and_split_list(result: Array, layout_node: DockableLayoutNode):
if layout_node is Layout.LayoutPanel: if layout_node is DockableLayoutPanel:
var nodes = [] var nodes: Array[Control] = []
for n in layout_node.names: for n in layout_node.names:
var node: Control = _children_names.get(n) var node: Control = _children_names.get(n)
if node: if node:
@ -343,21 +344,21 @@ func _calculate_panel_and_split_list(result: Array, layout_node: Layout.LayoutNo
node.visible = false node.visible = false
else: else:
nodes.append(node) nodes.append(node)
if nodes.empty(): if nodes.is_empty():
return null return null
else: else:
var panel = _get_panel(_current_panel_index) var panel := _get_panel(_current_panel_index)
_current_panel_index += 1 _current_panel_index += 1
panel.track_nodes(nodes, layout_node) panel.track_nodes(nodes, layout_node)
result.append(panel) result.append(panel)
return panel return panel
elif layout_node is Layout.LayoutSplit: elif layout_node is DockableLayoutSplit:
# by processing `second` before `first`, traversing `result` from back # by processing `second` before `first`, traversing `result` from back
# to front yields a nice pre-order tree traversal # to front yields a nice pre-order tree traversal
var second_result = _calculate_panel_and_split_list(result, layout_node.second) var second_result = _calculate_panel_and_split_list(result, layout_node.second)
var first_result = _calculate_panel_and_split_list(result, layout_node.first) var first_result = _calculate_panel_and_split_list(result, layout_node.first)
if first_result and second_result: if first_result and second_result:
var split = _get_split(_current_split_index) var split := _get_split(_current_split_index)
_current_split_index += 1 _current_split_index += 1
split.layout_split = layout_node split.layout_split = layout_node
split.first_minimum_size = first_result.get_layout_minimum_size() split.first_minimum_size = first_result.get_layout_minimum_size()
@ -372,58 +373,58 @@ func _calculate_panel_and_split_list(result: Array, layout_node: Layout.LayoutNo
push_warning("FIXME: invalid Resource, should be branch or leaf, found %s" % layout_node) push_warning("FIXME: invalid Resource, should be branch or leaf, found %s" % layout_node)
# Traverse list from back to front fitting controls where they belong. ## Traverse list from back to front fitting controls where they belong.
# ##
# Be sure to call this with the result from `_calculate_split_minimum_sizes`. ## Be sure to call this with the result from `_calculate_split_minimum_sizes`.
func _fit_panel_and_split_list_to_rect(panel_and_split_list: Array, rect: Rect2) -> void: func _fit_panel_and_split_list_to_rect(panel_and_split_list: Array, rect: Rect2) -> void:
var control = panel_and_split_list.pop_back() var control = panel_and_split_list.pop_back()
if control is DockablePanel: if control is DockablePanel:
_panel_container.fit_child_in_rect(control, rect) _panel_container.fit_child_in_rect(control, rect)
elif control is SplitHandle: elif control is SplitHandle:
var split_rects = control.get_split_rects(rect) var split_rects = control.get_split_rects(rect)
_split_container.fit_child_in_rect(control, split_rects.self) _split_container.fit_child_in_rect(control, split_rects["self"])
_fit_panel_and_split_list_to_rect(panel_and_split_list, split_rects.first) _fit_panel_and_split_list_to_rect(panel_and_split_list, split_rects["first"])
_fit_panel_and_split_list_to_rect(panel_and_split_list, split_rects.second) _fit_panel_and_split_list_to_rect(panel_and_split_list, split_rects["second"])
## Get the idx'th DockablePanel, reusing an instanced one if possible
func _get_panel(idx: int) -> DockablePanel: func _get_panel(idx: int) -> DockablePanel:
"""Get the idx'th DockablePanel, reusing an instanced one if possible"""
assert(_panel_container, "FIXME: creating panel without _panel_container") assert(_panel_container, "FIXME: creating panel without _panel_container")
if idx < _panel_container.get_child_count(): if idx < _panel_container.get_child_count():
return _panel_container.get_child(idx) return _panel_container.get_child(idx)
var panel = DockablePanel.new() var panel := DockablePanel.new()
panel.tab_align = _tab_align panel.tab_alignment = _tab_align
panel.tabs_visible = _tabs_visible panel.tabs_visible = _tabs_visible
panel.use_hidden_tabs_for_min_size = _use_hidden_tabs_for_min_size panel.use_hidden_tabs_for_min_size = _use_hidden_tabs_for_min_size
panel.set_tabs_rearrange_group(max(0, rearrange_group)) panel.set_tabs_rearrange_group(maxi(0, rearrange_group))
_panel_container.add_child(panel) _panel_container.add_child(panel)
panel.connect("tab_layout_changed", self, "_on_panel_tab_layout_changed", [panel]) panel.tab_layout_changed.connect(_on_panel_tab_layout_changed.bind(panel))
return panel return panel
## Get the idx'th SplitHandle, reusing an instanced one if possible
func _get_split(idx: int) -> SplitHandle: func _get_split(idx: int) -> SplitHandle:
"""Get the idx'th SplitHandle, reusing an instanced one if possible"""
assert(_split_container, "FIXME: creating split without _split_container") assert(_split_container, "FIXME: creating split without _split_container")
if idx < _split_container.get_child_count(): if idx < _split_container.get_child_count():
return _split_container.get_child(idx) return _split_container.get_child(idx)
var split = SplitHandle.new() var split := SplitHandle.new()
_split_container.add_child(split) _split_container.add_child(split)
return split return split
static func _untrack_children_after(node, idx: int) -> void: ## Helper for removing and freeing all remaining children from node
"""Helper for removing and freeing all remaining children from node""" func _untrack_children_after(node: Control, idx: int) -> void:
for i in range(idx, node.get_child_count()): for i in range(idx, node.get_child_count()):
var child = node.get_child(idx) var child := node.get_child(idx)
node.remove_child(child) node.remove_child(child)
child.queue_free() child.queue_free()
## Handler for `DockablePanel.tab_layout_changed`, update its DockableLayoutPanel
func _on_panel_tab_layout_changed(tab: int, panel: DockablePanel) -> void: func _on_panel_tab_layout_changed(tab: int, panel: DockablePanel) -> void:
"""Handler for `DockablePanel.tab_layout_changed`, update its LayoutPanel"""
_layout_dirty = true _layout_dirty = true
var control = panel.get_tab_control(tab) var control := panel.get_tab_control(tab)
if control is ReferenceControl: if control is DockableReferenceControl:
control = control.reference_to control = control.reference_to
if not _is_managed_node(control): if not _is_managed_node(control):
control.get_parent().remove_child(control) control.get_parent().remove_child(control)
@ -432,10 +433,10 @@ func _on_panel_tab_layout_changed(tab: int, panel: DockablePanel) -> void:
queue_sort() queue_sort()
## Handler for `Node.renamed` signal, updates tracked name for node
func _on_child_renamed(child: Node) -> void: func _on_child_renamed(child: Node) -> void:
"""Handler for `Node.renamed` signal, updates tracked name for node""" var old_name: String = _children_names.get(child)
var old_name = _children_names.get(child) if old_name == str(child.name):
if not old_name:
return return
_children_names.erase(old_name) _children_names.erase(old_name)
_children_names[child] = child.name _children_names[child] = child.name

View file

@ -1,14 +1,15 @@
tool @tool
extends TabContainer extends TabContainer
signal tab_layout_changed(tab) signal tab_layout_changed(tab)
const ReferenceControl = preload("dockable_panel_reference_control.gd") var leaf: DockableLayoutPanel:
const Layout = preload("layout.gd") get:
return get_leaf()
set(value):
set_leaf(value)
var leaf: Layout.LayoutPanel setget set_leaf, get_leaf var _leaf: DockableLayoutPanel
var _leaf: Layout.LayoutPanel
func _ready() -> void: func _ready() -> void:
@ -16,49 +17,51 @@ func _ready() -> void:
func _enter_tree() -> void: func _enter_tree() -> void:
connect("tab_selected", self, "_on_tab_selected") active_tab_rearranged.connect(_on_tab_changed)
connect("tab_changed", self, "_on_tab_changed") tab_selected.connect(_on_tab_selected)
tab_changed.connect(_on_tab_changed)
func _exit_tree() -> void: func _exit_tree() -> void:
disconnect("tab_selected", self, "_on_tab_selected") active_tab_rearranged.disconnect(_on_tab_changed)
disconnect("tab_changed", self, "_on_tab_changed") tab_selected.disconnect(_on_tab_selected)
tab_changed.disconnect(_on_tab_changed)
func track_nodes(nodes: Array, new_leaf: Layout.LayoutPanel) -> void: func track_nodes(nodes: Array[Control], new_leaf: DockableLayoutPanel) -> void:
_leaf = null # avoid using previous leaf in tab_changed signals _leaf = null # avoid using previous leaf in tab_changed signals
var min_size = min(nodes.size(), get_child_count()) var min_size := mini(nodes.size(), get_child_count())
# remove spare children # remove spare children
for i in range(min_size, get_child_count()): for i in range(min_size, get_child_count()):
var child = get_child(min_size) var child := get_child(min_size) as DockableReferenceControl
child.reference_to = null child.reference_to = null
remove_child(child) remove_child(child)
child.queue_free() child.queue_free()
# add missing children # add missing children
for i in range(min_size, nodes.size()): for i in range(min_size, nodes.size()):
var ref_control = ReferenceControl.new() var ref_control := DockableReferenceControl.new()
add_child(ref_control) add_child(ref_control)
assert(nodes.size() == get_child_count(), "FIXME") assert(nodes.size() == get_child_count(), "FIXME")
# setup children # setup children
for i in nodes.size(): for i in nodes.size():
var ref_control: ReferenceControl = get_child(i) var ref_control := get_child(i) as DockableReferenceControl
ref_control.reference_to = nodes[i] ref_control.reference_to = nodes[i]
set_tab_title(i, nodes[i].name) set_tab_title(i, nodes[i].name)
set_leaf(new_leaf) set_leaf(new_leaf)
func get_child_rect() -> Rect2: func get_child_rect() -> Rect2:
var control = get_current_tab_control() var control := get_current_tab_control()
return Rect2(rect_position + control.rect_position, control.rect_size) return Rect2(position + control.position, control.size)
func set_leaf(value: Layout.LayoutPanel) -> void: func set_leaf(value: DockableLayoutPanel) -> void:
if get_tab_count() > 0 and value: if get_tab_count() > 0 and value:
current_tab = clamp(value.current_tab, 0, get_tab_count() - 1) current_tab = clampi(value.current_tab, 0, get_tab_count() - 1)
_leaf = value _leaf = value
func get_leaf() -> Layout.LayoutPanel: func get_leaf() -> DockableLayoutPanel:
return _leaf return _leaf
@ -74,10 +77,10 @@ func _on_tab_selected(tab: int) -> void:
func _on_tab_changed(tab: int) -> void: func _on_tab_changed(tab: int) -> void:
if not _leaf: if not _leaf:
return return
var control = get_tab_control(tab) var control := get_tab_control(tab)
if not control: if not control:
return return
var tab_name = control.name var tab_name := control.name
var name_index_in_leaf = _leaf.find_name(tab_name) var name_index_in_leaf := _leaf.find_name(tab_name)
if name_index_in_leaf != tab: # NOTE: this handles added tabs (index == -1) if name_index_in_leaf != tab: # NOTE: this handles added tabs (index == -1)
emit_signal("tab_layout_changed", tab) tab_layout_changed.emit(tab)

View file

@ -1,20 +1,38 @@
tool @tool
class_name DockableReferenceControl
extends Container extends Container
# Control that mimics its own visibility and rect into another Control. ## Control that mimics its own visibility and rect into another Control.
var reference_to: Control setget set_reference_to, get_reference_to var reference_to: Control:
get:
return _reference_to
set(control):
if _reference_to != control:
if is_instance_valid(_reference_to):
_reference_to.renamed.disconnect(_on_reference_to_renamed)
_reference_to.minimum_size_changed.disconnect(update_minimum_size)
_reference_to = control
minimum_size_changed.emit()
if not is_instance_valid(_reference_to):
return
_reference_to.renamed.connect(_on_reference_to_renamed)
_reference_to.minimum_size_changed.connect(update_minimum_size)
_reference_to.visible = visible
_reposition_reference()
var _reference_to: Control = null var _reference_to: Control = null
func _ready() -> void: func _ready() -> void:
mouse_filter = MOUSE_FILTER_IGNORE mouse_filter = MOUSE_FILTER_IGNORE
set_notify_transform(true)
func _notification(what: int) -> void: func _notification(what: int) -> void:
if what == NOTIFICATION_VISIBILITY_CHANGED and _reference_to: if what == NOTIFICATION_VISIBILITY_CHANGED and _reference_to:
_reference_to.visible = visible _reference_to.visible = visible
elif what == NOTIFICATION_SORT_CHILDREN and _reference_to: elif what == NOTIFICATION_TRANSFORM_CHANGED and _reference_to:
_reposition_reference() _reposition_reference()
@ -22,27 +40,9 @@ func _get_minimum_size() -> Vector2:
return _reference_to.get_combined_minimum_size() if _reference_to else Vector2.ZERO return _reference_to.get_combined_minimum_size() if _reference_to else Vector2.ZERO
func set_reference_to(control: Control) -> void:
if _reference_to != control:
if _reference_to:
_reference_to.disconnect("renamed", self, "_on_reference_to_renamed")
_reference_to.disconnect("minimum_size_changed", self, "minimum_size_changed")
_reference_to = control
minimum_size_changed()
if not _reference_to:
return
_reference_to.connect("renamed", self, "_on_reference_to_renamed")
_reference_to.connect("minimum_size_changed", self, "minimum_size_changed")
_reference_to.visible = visible
func get_reference_to() -> Control:
return _reference_to
func _reposition_reference() -> void: func _reposition_reference() -> void:
_reference_to.rect_global_position = rect_global_position _reference_to.global_position = global_position
_reference_to.rect_size = rect_size _reference_to.size = size
func _on_reference_to_renamed() -> void: func _on_reference_to_renamed() -> void:

View file

@ -1,45 +1,48 @@
tool @tool
extends Control extends Control
const DRAW_NOTHING = -1 enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER }
const DRAW_CENTERED = -2
var _draw_margin = DRAW_NOTHING const DRAW_NOTHING := -1
var _should_split = false const DRAW_CENTERED := -2
const MARGIN_NONE := -1
var _draw_margin := DRAW_NOTHING
var _should_split := false
func _notification(what: int) -> void: func _notification(what: int) -> void:
if what == NOTIFICATION_MOUSE_EXIT: if what == NOTIFICATION_MOUSE_EXIT:
_draw_margin = DRAW_NOTHING _draw_margin = DRAW_NOTHING
update() queue_redraw()
elif what == NOTIFICATION_MOUSE_ENTER and not _should_split: elif what == NOTIFICATION_MOUSE_ENTER and not _should_split:
_draw_margin = DRAW_CENTERED _draw_margin = DRAW_CENTERED
update() queue_redraw()
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
if _should_split and event is InputEventMouseMotion: if _should_split and event is InputEventMouseMotion:
_draw_margin = _find_hover_margin(event.position) _draw_margin = _find_hover_margin(event.position)
update() queue_redraw()
func _draw() -> void: func _draw() -> void:
var rect var rect: Rect2
if _draw_margin == DRAW_NOTHING: if _draw_margin == DRAW_NOTHING:
return return
elif _draw_margin == DRAW_CENTERED: elif _draw_margin == DRAW_CENTERED:
rect = Rect2(Vector2.ZERO, rect_size) rect = Rect2(Vector2.ZERO, size)
elif _draw_margin == MARGIN_LEFT: elif _draw_margin == MARGIN_LEFT:
rect = Rect2(0, 0, rect_size.x * 0.5, rect_size.y) rect = Rect2(0, 0, size.x * 0.5, size.y)
elif _draw_margin == MARGIN_TOP: elif _draw_margin == MARGIN_TOP:
rect = Rect2(0, 0, rect_size.x, rect_size.y * 0.5) rect = Rect2(0, 0, size.x, size.y * 0.5)
elif _draw_margin == MARGIN_RIGHT: elif _draw_margin == MARGIN_RIGHT:
var half_width = rect_size.x * 0.5 var half_width = size.x * 0.5
rect = Rect2(half_width, 0, half_width, rect_size.y) rect = Rect2(half_width, 0, half_width, size.y)
elif _draw_margin == MARGIN_BOTTOM: elif _draw_margin == MARGIN_BOTTOM:
var half_height = rect_size.y * 0.5 var half_height = size.y * 0.5
rect = Rect2(0, half_height, rect_size.x, half_height) rect = Rect2(0, half_height, size.x, half_height)
var stylebox = get_stylebox("panel", "TooltipPanel") var stylebox := get_theme_stylebox("panel", "TooltipPanel")
draw_style_box(stylebox, rect) draw_style_box(stylebox, rect)
@ -48,7 +51,7 @@ func set_enabled(enabled: bool, should_split: bool = true) -> void:
_should_split = should_split _should_split = should_split
if enabled: if enabled:
_draw_margin = DRAW_NOTHING _draw_margin = DRAW_NOTHING
update() queue_redraw()
func get_hover_margin() -> int: func get_hover_margin() -> int:
@ -56,23 +59,23 @@ func get_hover_margin() -> int:
func _find_hover_margin(point: Vector2) -> int: func _find_hover_margin(point: Vector2) -> int:
var half_size = rect_size * 0.5 var half_size := size * 0.5
var left = point.distance_squared_to(Vector2(0, half_size.y)) var left := point.distance_squared_to(Vector2(0, half_size.y))
var lesser = left var lesser := left
var lesser_margin = MARGIN_LEFT var lesser_margin := MARGIN_LEFT
var top = point.distance_squared_to(Vector2(half_size.x, 0)) var top := point.distance_squared_to(Vector2(half_size.x, 0))
if lesser > top: if lesser > top:
lesser = top lesser = top
lesser_margin = MARGIN_TOP lesser_margin = MARGIN_TOP
var right = point.distance_squared_to(Vector2(rect_size.x, half_size.y)) var right := point.distance_squared_to(Vector2(size.x, half_size.y))
if lesser > right: if lesser > right:
lesser = right lesser = right
lesser_margin = MARGIN_RIGHT lesser_margin = MARGIN_RIGHT
var bottom = point.distance_squared_to(Vector2(half_size.x, rect_size.y)) var bottom := point.distance_squared_to(Vector2(half_size.x, size.y))
if lesser > bottom: if lesser > bottom:
#lesser = bottom # unused result #lesser = bottom # unused result
lesser_margin = MARGIN_BOTTOM lesser_margin = MARGIN_BOTTOM

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g>
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="4.001" y="4.016" width="4.005" height="5.004" rx="1" ry="1"/>
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="8.999" y="7.016" width="3.006" height="5.004" rx="1" ry="1"/>
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="4.004" y="10.023" width="4.005" height="1.99" rx="1" ry="1"/>
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="9" y="3.991" width="3.006" height="2.031" rx="1" ry="1"/>
</g>
<path d="M 13 1 C 14.097 1 15 1.903 15 3 L 15 13 C 15 14.097 14.097 15 13 15 L 3 15 C 1.903 15 1 14.097 1 13 L 1 3 C 1 1.903 1.903 1 3 1 L 13 1 Z M 3 13 L 13 13 L 13 3 L 3 3 L 3 13 Z" fill-rule="nonzero" style="fill-rule: nonzero; fill: rgb(142, 239, 152);"/>
</svg>

After

Width:  |  Height:  |  Size: 979 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dy25danh2am23"
path="res://.godot/imported/icon.svg-35635e7bbda4487d4b2942da1d987df8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/dockable_container/icon.svg"
dest_files=["res://.godot/imported/icon.svg-35635e7bbda4487d4b2942da1d987df8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,17 +1,22 @@
extends EditorInspectorPlugin extends EditorInspectorPlugin
const DockableContainer = preload("../dockable_container.gd") const LayoutEditorProperty := preload("layout_editor_property.gd")
const LayoutEditorProperty = preload("layout_editor_property.gd")
func can_handle(object: Object) -> bool: func _can_handle(object: Object) -> bool:
return object is DockableContainer return object is DockableContainer
func parse_property( func _parse_property(
_object: Object, _type: int, path: String, _hint: int, _hint_text: String, _usage: int _object: Object,
_type: Variant.Type,
name: String,
_hint: PropertyHint,
_hint_text: String,
_usage: int,
_wide: bool
) -> bool: ) -> bool:
if path == "layout": if name == "layout":
var editor_property = LayoutEditorProperty.new() var editor_property := LayoutEditorProperty.new()
add_property_editor("layout", editor_property) add_property_editor("layout", editor_property)
return false return false

View file

@ -1,71 +1,71 @@
extends EditorProperty extends EditorProperty
const DockableContainer = preload("../dockable_container.gd") var _container := DockableContainer.new()
const Layout = preload("../layout.gd") var _hidden_menu_button := MenuButton.new()
var _container = DockableContainer.new()
var _hidden_menu_button = MenuButton.new()
var _hidden_menu_popup: PopupMenu var _hidden_menu_popup: PopupMenu
var _hidden_menu_list: PoolStringArray var _hidden_menu_list: PackedStringArray
func _ready() -> void: func _ready() -> void:
rect_min_size = Vector2(128, 256) custom_minimum_size = Vector2(128, 256)
_hidden_menu_button.text = "Visible nodes" _hidden_menu_button.text = "Visible nodes"
add_child(_hidden_menu_button) add_child(_hidden_menu_button)
_hidden_menu_popup = _hidden_menu_button.get_popup() _hidden_menu_popup = _hidden_menu_button.get_popup()
_hidden_menu_popup.hide_on_checkable_item_selection = false _hidden_menu_popup.hide_on_checkable_item_selection = false
_hidden_menu_popup.connect("about_to_show", self, "_on_hidden_menu_popup_about_to_show") _hidden_menu_popup.about_to_popup.connect(_on_hidden_menu_popup_about_to_show)
_hidden_menu_popup.connect("id_pressed", self, "_on_hidden_menu_popup_id_pressed") _hidden_menu_popup.id_pressed.connect(_on_hidden_menu_popup_id_pressed)
_container.clone_layout_on_ready = false _container.clone_layout_on_ready = false
_container.rect_min_size = rect_min_size _container.custom_minimum_size = custom_minimum_size
var original_container: DockableContainer = get_edited_object() var value := _get_layout().clone() # The layout gets reset when selecting it without clone
var value = original_container.get(get_edited_property())
_container.set(get_edited_property(), value)
for n in value.get_names(): for n in value.get_names():
var child = _create_child_control(n) var child := _create_child_control(n)
_container.add_child(child) _container.add_child(child)
_container.set(get_edited_property(), value)
add_child(_container) add_child(_container)
set_bottom_editor(_container) set_bottom_editor(_container)
func update_property() -> void: func _exit_tree() -> void: # Not sure if this is needed, but just to be sure
var value = _get_layout() queue_free()
func _update_property() -> void:
var value := _get_layout()
_container.set(get_edited_property(), value) _container.set(get_edited_property(), value)
func _get_layout() -> Layout: func _get_layout() -> DockableLayout:
var original_container: DockableContainer = get_edited_object() var original_container := get_edited_object() as DockableContainer
return original_container.get(get_edited_property()) return original_container.get(get_edited_property())
func _create_child_control(named: String) -> Control: func _create_child_control(named: String) -> Label:
var new_control = Label.new() var new_control := Label.new()
new_control.name = named new_control.name = named
new_control.align = Label.ALIGN_CENTER new_control.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
new_control.valign = Label.VALIGN_CENTER new_control.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
new_control.clip_text = true new_control.clip_text = true
new_control.text = named new_control.text = named
return new_control return new_control
func _on_hidden_menu_popup_about_to_show() -> void: func _on_hidden_menu_popup_about_to_show() -> void:
var layout = _get_layout() var layout := _get_layout().clone()
_hidden_menu_popup.clear() _hidden_menu_popup.clear()
_hidden_menu_list = layout.get_names() _hidden_menu_list = layout.get_names()
for i in _hidden_menu_list.size(): for i in _hidden_menu_list.size():
var tab_name = _hidden_menu_list[i] var tab_name := _hidden_menu_list[i]
_hidden_menu_popup.add_check_item(tab_name, i) _hidden_menu_popup.add_check_item(tab_name, i)
_hidden_menu_popup.set_item_checked(i, not layout.is_tab_hidden(tab_name)) _hidden_menu_popup.set_item_checked(i, not layout.is_tab_hidden(tab_name))
func _on_hidden_menu_popup_id_pressed(id: int) -> void: func _on_hidden_menu_popup_id_pressed(id: int) -> void:
var layout = _get_layout() var layout := _get_layout().clone()
var tab_name = _hidden_menu_list[id] var tab_name := _hidden_menu_list[id]
var new_hidden = not layout.is_tab_hidden(tab_name) var new_hidden := not layout.is_tab_hidden(tab_name)
layout.set_tab_hidden(tab_name, new_hidden) _get_layout().set_tab_hidden(tab_name, new_hidden)
_hidden_menu_popup.set_item_checked(id, not new_hidden) _hidden_menu_popup.set_item_checked(id, not new_hidden)
emit_changed(get_edited_property(), layout) emit_changed(get_edited_property(), _get_layout()) # This line may not be needed

View file

@ -1,84 +1,80 @@
tool @tool
class_name DockableLayout
extends Resource extends Resource
# Layout Resource definition, holding the root LayoutNode and hidden tabs. ## DockableLayout Resource definition, holding the root DockableLayoutNode and hidden tabs.
# ##
# LayoutSplit are binary trees with nested LayoutSplit subtrees and LayoutPanel ## DockableLayoutSplit are binary trees with nested DockableLayoutSplit subtrees
# leaves. Both of them inherit from LayoutNode to help with type annotation and ## and DockableLayoutPanel leaves. Both of them inherit from DockableLayoutNode to help with
# define common funcionality. ## type annotation and define common functionality.
# ##
# Hidden tabs are marked in the `hidden_tabs` Dictionary by name. ## Hidden tabs are marked in the `hidden_tabs` Dictionary by name.
const LayoutNode = preload("layout_node.gd") enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER }
const LayoutPanel = preload("layout_panel.gd")
const LayoutSplit = preload("layout_split.gd")
export(Resource) var root = LayoutPanel.new() setget set_root, get_root @export var root: DockableLayoutNode = DockableLayoutPanel.new():
export(Dictionary) var hidden_tabs = {} setget set_hidden_tabs, get_hidden_tabs get:
return _root
set(value):
set_root(value)
@export var hidden_tabs := {}:
get:
return _hidden_tabs
set(value):
if value != _hidden_tabs:
_hidden_tabs = value
changed.emit()
var _changed_signal_queued = false var _changed_signal_queued := false
var _first_leaf: LayoutPanel var _first_leaf: DockableLayoutPanel
var _hidden_tabs: Dictionary var _hidden_tabs: Dictionary
var _leaf_by_node_name: Dictionary var _leaf_by_node_name: Dictionary
var _root: LayoutNode = LayoutPanel.new() var _root: DockableLayoutNode = DockableLayoutPanel.new()
func _init() -> void: func _init() -> void:
resource_name = "Layout" resource_name = "Layout"
func set_root(value: LayoutNode, should_emit_changed = true) -> void: func set_root(value: DockableLayoutNode, should_emit_changed := true) -> void:
if not value: if not value:
value = LayoutPanel.new() value = DockableLayoutPanel.new()
if _root == value: if _root == value:
return return
if _root and _root.is_connected("changed", self, "_on_root_changed"): if _root and _root.changed.is_connected(_on_root_changed):
_root.disconnect("changed", self, "_on_root_changed") _root.changed.disconnect(_on_root_changed)
_root = value _root = value
_root.parent = null _root.parent = null
_root.connect("changed", self, "_on_root_changed") _root.changed.connect(_on_root_changed)
if should_emit_changed: if should_emit_changed:
_on_root_changed() _on_root_changed()
func get_root() -> LayoutNode: func get_root() -> DockableLayoutNode:
return _root return _root
func set_hidden_tabs(value: Dictionary) -> void: func clone() -> DockableLayout:
if value != _hidden_tabs: return duplicate(true)
_hidden_tabs = value
emit_signal("changed")
func get_hidden_tabs() -> Dictionary: func get_names() -> PackedStringArray:
return _hidden_tabs
func clone():
var new_layout = get_script().new()
new_layout.root = _root.clone()
new_layout._hidden_tabs = _hidden_tabs.duplicate()
return new_layout
func get_names() -> PoolStringArray:
return _root.get_names() return _root.get_names()
# Add missing nodes on first leaf and remove nodes outside indices from leaves. ## Add missing nodes on first leaf and remove nodes outside indices from leaves.
# ##
# _leaf_by_node_name = { ## _leaf_by_node_name = {
# (string keys) = respective Leaf that holds the node name, ## (string keys) = respective Leaf that holds the node name,
# } ## }
func update_nodes(names: PoolStringArray) -> void: func update_nodes(names: PackedStringArray) -> void:
_leaf_by_node_name.clear() _leaf_by_node_name.clear()
_first_leaf = null _first_leaf = null
var empty_leaves = [] var empty_leaves: Array[DockableLayoutPanel] = []
_ensure_names_in_node(_root, names, empty_leaves) _ensure_names_in_node(_root, names, empty_leaves) # Changes _leaf_by_node_name and empty_leaves
for l in empty_leaves: for l in empty_leaves:
_remove_leaf(l) _remove_leaf(l)
if not _first_leaf: if not _first_leaf:
_first_leaf = LayoutPanel.new() _first_leaf = DockableLayoutPanel.new()
set_root(_first_leaf) set_root(_first_leaf)
for n in names: for n in names:
if not _leaf_by_node_name.has(n): if not _leaf_by_node_name.has(n):
@ -87,12 +83,12 @@ func update_nodes(names: PoolStringArray) -> void:
_on_root_changed() _on_root_changed()
func move_node_to_leaf(node: Node, leaf: LayoutPanel, relative_position: int) -> void: func move_node_to_leaf(node: Node, leaf: DockableLayoutPanel, relative_position: int) -> void:
var node_name = node.name var node_name := node.name
var previous_leaf = _leaf_by_node_name.get(node_name) var previous_leaf: DockableLayoutPanel = _leaf_by_node_name.get(node_name)
if previous_leaf: if previous_leaf:
previous_leaf.remove_node(node) previous_leaf.remove_node(node)
if previous_leaf.empty(): if previous_leaf.is_empty():
_remove_leaf(previous_leaf) _remove_leaf(previous_leaf)
leaf.insert_node(relative_position, node) leaf.insert_node(relative_position, node)
@ -100,18 +96,18 @@ func move_node_to_leaf(node: Node, leaf: LayoutPanel, relative_position: int) ->
_on_root_changed() _on_root_changed()
func get_leaf_for_node(node: Node) -> LayoutPanel: func get_leaf_for_node(node: Node) -> DockableLayoutPanel:
return _leaf_by_node_name.get(node.name) return _leaf_by_node_name.get(node.name)
func split_leaf_with_node(leaf, node: Node, margin: int) -> void: func split_leaf_with_node(leaf: DockableLayoutPanel, node: Node, margin: int) -> void:
var root_branch = leaf.parent var root_branch := leaf.parent
var new_leaf = LayoutPanel.new() var new_leaf := DockableLayoutPanel.new()
var new_branch = LayoutSplit.new() var new_branch := DockableLayoutSplit.new()
if margin == MARGIN_LEFT or margin == MARGIN_RIGHT: if margin == MARGIN_LEFT or margin == MARGIN_RIGHT:
new_branch.direction = LayoutSplit.Direction.HORIZONTAL new_branch.direction = DockableLayoutSplit.Direction.HORIZONTAL
else: else:
new_branch.direction = LayoutSplit.Direction.VERTICAL new_branch.direction = DockableLayoutSplit.Direction.VERTICAL
if margin == MARGIN_LEFT or margin == MARGIN_TOP: if margin == MARGIN_LEFT or margin == MARGIN_TOP:
new_branch.first = new_leaf new_branch.first = new_leaf
new_branch.second = leaf new_branch.second = leaf
@ -130,7 +126,7 @@ func split_leaf_with_node(leaf, node: Node, margin: int) -> void:
func add_node(node: Node) -> void: func add_node(node: Node) -> void:
var node_name = node.name var node_name := node.name
if _leaf_by_node_name.has(node_name): if _leaf_by_node_name.has(node_name):
return return
_first_leaf.push_name(node_name) _first_leaf.push_name(node_name)
@ -139,19 +135,19 @@ func add_node(node: Node) -> void:
func remove_node(node: Node) -> void: func remove_node(node: Node) -> void:
var node_name = node.name var node_name := node.name
var leaf: LayoutPanel = _leaf_by_node_name.get(node_name) var leaf: DockableLayoutPanel = _leaf_by_node_name.get(node_name)
if not leaf: if not leaf:
return return
leaf.remove_node(node) leaf.remove_node(node)
_leaf_by_node_name.erase(node_name) _leaf_by_node_name.erase(node_name)
if leaf.empty(): if leaf.is_empty():
_remove_leaf(leaf) _remove_leaf(leaf)
_on_root_changed() _on_root_changed()
func rename_node(previous_name: String, new_name: String) -> void: func rename_node(previous_name: String, new_name: String) -> void:
var leaf = _leaf_by_node_name.get(previous_name) var leaf: DockableLayoutPanel = _leaf_by_node_name.get(previous_name)
if not leaf: if not leaf:
return return
leaf.rename_node(previous_name, new_name) leaf.rename_node(previous_name, new_name)
@ -187,35 +183,35 @@ func _on_root_changed() -> void:
return return
_changed_signal_queued = true _changed_signal_queued = true
set_deferred("_changed_signal_queued", false) set_deferred("_changed_signal_queued", false)
call_deferred("emit_signal", "changed") emit_changed.call_deferred()
func _ensure_names_in_node(node: LayoutNode, names: PoolStringArray, empty_leaves: Array) -> void: func _ensure_names_in_node(
if node is LayoutPanel: node: DockableLayoutNode, names: PackedStringArray, empty_leaves: Array[DockableLayoutPanel]
node.update_nodes(names, _leaf_by_node_name) ) -> void:
if node.empty(): if node is DockableLayoutPanel:
node.update_nodes(names, _leaf_by_node_name) # This changes _leaf_by_node_name
if node.is_empty():
empty_leaves.append(node) empty_leaves.append(node)
if not _first_leaf: if not _first_leaf:
_first_leaf = node _first_leaf = node
elif node is LayoutSplit: elif node is DockableLayoutSplit:
_ensure_names_in_node(node.first, names, empty_leaves) _ensure_names_in_node(node.first, names, empty_leaves)
_ensure_names_in_node(node.second, names, empty_leaves) _ensure_names_in_node(node.second, names, empty_leaves)
else: else:
assert(false, "Invalid Resource, should be branch or leaf, found %s" % node) assert(false, "Invalid Resource, should be branch or leaf, found %s" % node)
func _remove_leaf(leaf: LayoutPanel) -> void: func _remove_leaf(leaf: DockableLayoutPanel) -> void:
assert(leaf.empty(), "FIXME: trying to remove a leaf with nodes") assert(leaf.is_empty(), "FIXME: trying to remove_at a leaf with nodes")
if _root == leaf: if _root == leaf:
return return
var collapsed_branch = leaf.parent var collapsed_branch := leaf.parent
assert(collapsed_branch is LayoutSplit, "FIXME: leaf is not a child of branch") assert(collapsed_branch is DockableLayoutSplit, "FIXME: leaf is not a child of branch")
var kept_branch = ( var kept_branch: DockableLayoutNode = (
collapsed_branch.first collapsed_branch.first if leaf == collapsed_branch.second else collapsed_branch.second
if leaf == collapsed_branch.second
else collapsed_branch.second
) )
var root_branch = collapsed_branch.parent var root_branch := collapsed_branch.parent #HERE
if collapsed_branch == _root: if collapsed_branch == _root:
set_root(kept_branch, true) set_root(kept_branch, true)
elif root_branch: elif root_branch:
@ -231,10 +227,10 @@ func _print_tree() -> void:
print("") print("")
func _print_tree_step(tree_or_leaf, level, idx) -> void: func _print_tree_step(tree_or_leaf: DockableLayoutNode, level: int, idx: int) -> void:
if tree_or_leaf is LayoutPanel: if tree_or_leaf is DockableLayoutPanel:
print(" |".repeat(level), "- (%d) = " % idx, tree_or_leaf.names) print(" |".repeat(level), "- (%d) = " % idx, tree_or_leaf.names)
else: elif tree_or_leaf is DockableLayoutSplit:
print( print(
" |".repeat(level), " |".repeat(level),
"-+ (%d) = " % idx, "-+ (%d) = " % idx,

View file

@ -1,30 +1,23 @@
tool @tool
class_name DockableLayoutNode
extends Resource extends Resource
# Base class for Layout tree nodes ## Base class for DockableLayout tree nodes
var parent = null var parent: DockableLayoutSplit = null
func emit_tree_changed() -> void: func emit_tree_changed() -> void:
var node = self var node := self
while node: while node:
node.emit_signal("changed") node.emit_changed()
node = node.parent node = node.parent
# Returns a deep copy of the layout. ## Returns whether there are any nodes
# func is_empty() -> bool:
# Use this instead of `Resource.duplicate(true)` to ensure objects have the
# right script and parenting is correctly set for each node.
func clone():
assert(false, "FIXME: implement on child")
# Returns whether there are any nodes
func empty() -> bool:
return true return true
# Returns all tab names in this node ## Returns all tab names in this node
func get_names() -> PoolStringArray: func get_names() -> PackedStringArray:
return PoolStringArray() return PackedStringArray()

View file

@ -1,11 +1,23 @@
tool @tool
extends "layout_node.gd" class_name DockableLayoutPanel
# Layout leaf nodes, defining tabs extends DockableLayoutNode
## DockableLayout leaf nodes, defining tabs
export(PoolStringArray) var names: PoolStringArray setget set_names, get_names @export var names: PackedStringArray:
export(int) var current_tab: int setget set_current_tab, get_current_tab get:
return get_names()
set(value):
_names = value
emit_tree_changed()
@export var current_tab: int:
get:
return int(clamp(_current_tab, 0, _names.size() - 1))
set(value):
if value != _current_tab:
_current_tab = value
emit_tree_changed()
var _names := PoolStringArray() var _names := PackedStringArray()
var _current_tab := 0 var _current_tab := 0
@ -13,29 +25,8 @@ func _init() -> void:
resource_name = "Tabs" resource_name = "Tabs"
func clone(): ## Returns all tab names in this node
var new_panel = get_script().new() func get_names() -> PackedStringArray:
new_panel._names = _names
new_panel._current_tab = _current_tab
return new_panel
func set_current_tab(value: int) -> void:
if value != _current_tab:
_current_tab = value
emit_tree_changed()
func get_current_tab() -> int:
return int(clamp(_current_tab, 0, _names.size() - 1))
func set_names(value: PoolStringArray) -> void:
_names = value
emit_tree_changed()
func get_names() -> PoolStringArray:
return _names return _names
@ -56,21 +47,21 @@ func find_name(node_name: String) -> int:
return -1 return -1
func find_node(node: Node): func find_child(node: Node) -> int:
return find_name(node.name) return find_name(node.name)
func remove_node(node: Node) -> void: func remove_node(node: Node) -> void:
var i = find_node(node) var i := find_child(node)
if i >= 0: if i >= 0:
_names.remove(i) _names.remove_at(i)
emit_tree_changed() emit_tree_changed()
else: else:
push_warning("Remove failed, node '%s' was not found" % node) push_warning("Remove failed, node '%s' was not found" % node)
func rename_node(previous_name: String, new_name: String) -> void: func rename_node(previous_name: String, new_name: String) -> void:
var i = find_name(previous_name) var i := find_name(previous_name)
if i >= 0: if i >= 0:
_names.set(i, new_name) _names.set(i, new_name)
emit_tree_changed() emit_tree_changed()
@ -78,17 +69,18 @@ func rename_node(previous_name: String, new_name: String) -> void:
push_warning("Rename failed, name '%s' was not found" % previous_name) push_warning("Rename failed, name '%s' was not found" % previous_name)
func empty() -> bool: ## Returns whether there are any nodes
return _names.empty() func is_empty() -> bool:
return _names.is_empty()
func update_nodes(node_names: PoolStringArray, data: Dictionary): func update_nodes(node_names: PackedStringArray, data: Dictionary) -> void:
var i = 0 var i := 0
var removed_any = false var removed_any := false
while i < _names.size(): while i < _names.size():
var current = _names[i] var current := _names[i]
if not current in node_names or data.has(current): if not current in node_names or data.has(current):
_names.remove(i) _names.remove_at(i)
removed_any = true removed_any = true
else: else:
data[current] = self data[current] = self

View file

@ -1,76 +1,77 @@
tool @tool
extends "layout_node.gd" class_name DockableLayoutSplit
# Layout binary tree nodes, defining subtrees and leaf panels extends DockableLayoutNode
## DockableLayout binary tree nodes, defining subtrees and leaf panels
enum Direction { enum Direction { HORIZONTAL, VERTICAL }
HORIZONTAL,
VERTICAL,
}
const LayoutPanel = preload("layout_panel.gd") @export var direction := Direction.HORIZONTAL:
get:
return get_direction()
set(value):
set_direction(value)
@export_range(0, 1) var percent := 0.5:
get = get_percent,
set = set_percent
@export var first: DockableLayoutNode = DockableLayoutPanel.new():
get:
return get_first()
set(value):
set_first(value)
@export var second: DockableLayoutNode = DockableLayoutPanel.new():
get:
return get_second()
set(value):
set_second(value)
export(Direction) var direction = Direction.HORIZONTAL setget set_direction, get_direction var _direction := Direction.HORIZONTAL
export(float, 0, 1) var percent = 0.5 setget set_percent, get_percent var _percent := 0.5
export(Resource) var first = LayoutPanel.new() setget set_first, get_first var _first: DockableLayoutNode
export(Resource) var second = LayoutPanel.new() setget set_second, get_second var _second: DockableLayoutNode
var _direction = Direction.HORIZONTAL
var _percent = 0.5
var _first
var _second
func _init() -> void: func _init() -> void:
resource_name = "Split" resource_name = "Split"
func clone(): func set_first(value: DockableLayoutNode) -> void:
var new_split = get_script().new()
new_split._direction = _direction
new_split._percent = _percent
new_split.first = _first.clone()
new_split.second = _second.clone()
return new_split
func set_first(value) -> void:
if value == null: if value == null:
_first = LayoutPanel.new() _first = DockableLayoutPanel.new()
else: else:
_first = value _first = value
_first.parent = self _first.parent = self
emit_tree_changed() emit_tree_changed()
func get_first(): func get_first() -> DockableLayoutNode:
return _first return _first
func set_second(value) -> void: func set_second(value: DockableLayoutNode) -> void:
if value == null: if value == null:
_second = LayoutPanel.new() _second = DockableLayoutPanel.new()
else: else:
_second = value _second = value
_second.parent = self _second.parent = self
emit_tree_changed() emit_tree_changed()
func get_second(): func get_second() -> DockableLayoutNode:
return _second return _second
func set_direction(value: int) -> void: func set_direction(value: Direction) -> void:
if value != _direction: if value != _direction:
_direction = value _direction = value
emit_tree_changed() emit_tree_changed()
func get_direction() -> int: func get_direction() -> Direction:
return _direction return _direction
func set_percent(value: float) -> void: func set_percent(value: float) -> void:
var clamped_value = clamp(value, 0, 1) var clamped_value := clampf(value, 0, 1)
if not is_equal_approx(_percent, clamped_value): if not is_equal_approx(_percent, clamped_value):
_percent = clamped_value _percent = clamped_value
emit_tree_changed() emit_tree_changed()
@ -80,14 +81,15 @@ func get_percent() -> float:
return _percent return _percent
func get_names() -> PoolStringArray: func get_names() -> PackedStringArray:
var names = _first.get_names() var names := _first.get_names()
names.append_array(_second.get_names()) names.append_array(_second.get_names())
return names return names
func empty() -> bool: ## Returns whether there are any nodes
return _first.empty() and _second.empty() func is_empty() -> bool:
return _first.is_empty() and _second.is_empty()
func is_horizontal() -> bool: func is_horizontal() -> bool:

View file

@ -9,5 +9,5 @@ Layout information is stored in Resource objects, so they can be saved/loaded fr
This plugin also offers a replica of the Container layout to be edited directly in the inspector." This plugin also offers a replica of the Container layout to be edited directly in the inspector."
author="gilzoide" author="gilzoide"
version="1.1.1" version="1.1.2"
script="plugin.gd" script="plugin.gd"

View file

@ -1,15 +1,15 @@
tool @tool
extends EditorPlugin extends EditorPlugin
const DockableContainer = preload("dockable_container.gd") const LayoutInspectorPlugin := preload("inspector_plugin/editor_inspector_plugin.gd")
const LayoutInspectorPlugin = preload("inspector_plugin/editor_inspector_plugin.gd") const Icon := preload("icon.svg")
var _layout_inspector_plugin var _layout_inspector_plugin: LayoutInspectorPlugin
func _enter_tree() -> void: func _enter_tree() -> void:
_layout_inspector_plugin = LayoutInspectorPlugin.new() _layout_inspector_plugin = LayoutInspectorPlugin.new()
add_custom_type("DockableContainer", "Container", DockableContainer, null) add_custom_type("DockableContainer", "Container", DockableContainer, Icon)
add_inspector_plugin(_layout_inspector_plugin) add_inspector_plugin(_layout_inspector_plugin)

View file

@ -1,10 +1,10 @@
extends VBoxContainer extends VBoxContainer
const SAVED_LAYOUT_PATH = "user://layout.tres" const SAVED_LAYOUT_PATH := "user://layout.tres"
onready var _container = $DockableContainers/DockableContainer @onready var _container := $DockableContainers/DockableContainer as DockableContainer
onready var _clone_control = $HBoxContainer/ControlPrefab @onready var _clone_control := $HBoxContainer/ControlPrefab as ColorRect
onready var _checkbox_container = $HBoxContainer @onready var _checkbox_container := $HBoxContainer as HBoxContainer
func _ready() -> void: func _ready() -> void:
@ -12,33 +12,33 @@ func _ready() -> void:
$HBoxContainer/SaveLayoutButton.visible = false $HBoxContainer/SaveLayoutButton.visible = false
$HBoxContainer/LoadLayoutButton.visible = false $HBoxContainer/LoadLayoutButton.visible = false
var tabs = _container.get_tabs() var tabs := _container.get_tabs()
for i in tabs.size(): for i in tabs.size():
var checkbox = CheckBox.new() var checkbox := CheckBox.new()
checkbox.text = str(i) checkbox.text = str(i)
checkbox.pressed = not _container.is_control_hidden(tabs[i]) checkbox.button_pressed = not _container.is_control_hidden(tabs[i])
checkbox.connect("toggled", self, "_on_CheckButton_toggled", [tabs[i]]) checkbox.toggled.connect(_on_CheckButton_toggled.bind(tabs[i]))
_checkbox_container.add_child(checkbox) _checkbox_container.add_child(checkbox)
func _on_add_pressed() -> void: func _on_add_pressed() -> void:
var control = _clone_control.duplicate() var control := _clone_control.duplicate()
control.get_node("Buttons/Rename").connect( control.get_node("Buttons/Rename").pressed.connect(
"pressed", self, "_on_control_rename_button_pressed", [control] _on_control_rename_button_pressed.bind(control)
) )
control.get_node("Buttons/Remove").connect( control.get_node("Buttons/Remove").pressed.connect(
"pressed", self, "_on_control_remove_button_pressed", [control] _on_control_remove_button_pressed.bind(control)
) )
control.color = Color(randf(), randf(), randf()) control.color = Color(randf(), randf(), randf())
control.name = "Control0" control.name = "Control0"
_container.add_child(control, true) _container.add_child(control, true)
yield(_container, "sort_children") await _container.sort_children
_container.set_control_as_current_tab(control) _container.set_control_as_current_tab(control)
func _on_save_pressed() -> void: func _on_save_pressed() -> void:
if ResourceSaver.save(SAVED_LAYOUT_PATH, _container.get_layout()) != OK: if ResourceSaver.save(_container.layout, SAVED_LAYOUT_PATH) != OK:
print("ERROR") print("ERROR")
@ -51,11 +51,11 @@ func _on_load_pressed() -> void:
func _on_control_rename_button_pressed(control: Control) -> void: func _on_control_rename_button_pressed(control: Control) -> void:
control.name += " =D" control.name = StringName(str(control.name) + " =D")
func _on_control_remove_button_pressed(control: Control) -> void: func _on_control_remove_button_pressed(control: Control) -> void:
_container.remove_child(control) control.get_parent().remove_child(control)
control.queue_free() control.queue_free()

View file

@ -1,235 +1,176 @@
[gd_scene load_steps=16 format=2] [gd_scene load_steps=16 format=3 uid="uid://drlvhuchtk6if"]
[ext_resource path="res://addons/dockable_container/dockable_container.gd" type="Script" id=1] [ext_resource type="Script" path="res://addons/dockable_container/dockable_container.gd" id="1"]
[ext_resource path="res://addons/dockable_container/layout.gd" type="Script" id=2] [ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="2"]
[ext_resource path="res://addons/dockable_container/layout_split.gd" type="Script" id=3] [ext_resource type="Script" path="res://addons/dockable_container/samples/TestScene.gd" id="4"]
[ext_resource path="res://addons/dockable_container/samples/TestScene.gd" type="Script" id=4] [ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="4_yhgfb"]
[ext_resource path="res://addons/dockable_container/layout_panel.gd" type="Script" id=5] [ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="5"]
[sub_resource type="Resource" id=5] [sub_resource type="Resource" id="Resource_8aoc2"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource( 5 ) script = ExtResource("5")
names = PoolStringArray( "Control0" ) names = PackedStringArray("Control0")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id=1] [sub_resource type="Resource" id="Resource_6kjom"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource( 5 ) script = ExtResource("5")
names = PoolStringArray( "Control1", "Control2" ) names = PackedStringArray("Control1", "Control2")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id=6] [sub_resource type="Resource" id="Resource_hl8y1"]
resource_name = "Split" resource_name = "Split"
script = ExtResource( 3 ) script = ExtResource("4_yhgfb")
direction = 1 direction = 1
percent = 0.5 percent = 0.5
first = SubResource( 5 ) first = SubResource("Resource_8aoc2")
second = SubResource( 1 ) second = SubResource("Resource_6kjom")
[sub_resource type="Resource" id=2] [sub_resource type="Resource" id="Resource_ybwqe"]
resource_name = "Layout" resource_name = "Layout"
script = ExtResource( 2 ) script = ExtResource("2")
root = SubResource( 6 ) root = SubResource("Resource_hl8y1")
hidden_tabs = { hidden_tabs = {}
}
[sub_resource type="Resource" id=3] [sub_resource type="Resource" id="Resource_ntwfj"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource( 5 ) script = ExtResource("5")
names = PoolStringArray( "Control3" ) names = PackedStringArray("Control3")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id=7] [sub_resource type="Resource" id="Resource_dmyvf"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource( 5 ) script = ExtResource("5")
names = PoolStringArray( "Control4" ) names = PackedStringArray("Control4")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id=8] [sub_resource type="Resource" id="Resource_vag66"]
resource_name = "Split" resource_name = "Split"
script = ExtResource( 3 ) script = ExtResource("4_yhgfb")
direction = 1 direction = 1
percent = 0.28125 percent = 0.281
first = SubResource( 3 ) first = SubResource("Resource_ntwfj")
second = SubResource( 7 ) second = SubResource("Resource_dmyvf")
[sub_resource type="Resource" id=9] [sub_resource type="Resource" id="Resource_4q660"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource( 5 ) script = ExtResource("5")
names = PoolStringArray( "Control5" ) names = PackedStringArray("Control5")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id=10] [sub_resource type="Resource" id="Resource_jhibs"]
resource_name = "Split" resource_name = "Split"
script = ExtResource( 3 ) script = ExtResource("4_yhgfb")
direction = 0 direction = 0
percent = 0.5 percent = 0.5
first = SubResource( 8 ) first = SubResource("Resource_vag66")
second = SubResource( 9 ) second = SubResource("Resource_4q660")
[sub_resource type="Resource" id=4] [sub_resource type="Resource" id="Resource_xhxpg"]
resource_name = "Layout" resource_name = "Layout"
script = ExtResource( 2 ) script = ExtResource("2")
root = SubResource( 10 ) root = SubResource("Resource_jhibs")
hidden_tabs = { hidden_tabs = {}
}
[node name="SampleScene" type="VBoxContainer"] [node name="SampleScene" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
script = ExtResource( 4 ) script = ExtResource("4")
[node name="HBoxContainer" type="HBoxContainer" parent="."] [node name="HBoxContainer" type="HBoxContainer" parent="."]
margin_right = 1024.0 layout_mode = 2
margin_bottom = 20.0
custom_constants/separation = 16
alignment = 1 alignment = 1
[node name="AddControlButton" type="Button" parent="HBoxContainer"] [node name="AddControlButton" type="Button" parent="HBoxContainer"]
margin_left = 345.0 layout_mode = 2
margin_right = 472.0
margin_bottom = 20.0
size_flags_horizontal = 0 size_flags_horizontal = 0
size_flags_vertical = 4 size_flags_vertical = 4
text = "(+) ADD CONTROL" text = "(+) ADD CONTROL"
[node name="SaveLayoutButton" type="Button" parent="HBoxContainer"] [node name="SaveLayoutButton" type="Button" parent="HBoxContainer"]
margin_left = 488.0 layout_mode = 2
margin_right = 575.0
margin_bottom = 20.0
size_flags_horizontal = 0 size_flags_horizontal = 0
size_flags_vertical = 4 size_flags_vertical = 4
text = "Save Layout" text = "Save Layout"
[node name="LoadLayoutButton" type="Button" parent="HBoxContainer"] [node name="LoadLayoutButton" type="Button" parent="HBoxContainer"]
margin_left = 591.0 layout_mode = 2
margin_right = 679.0
margin_bottom = 20.0
size_flags_horizontal = 0 size_flags_horizontal = 0
size_flags_vertical = 4 size_flags_vertical = 4
text = "Load Layout" text = "Load Layout"
[node name="ControlPrefab" type="ColorRect" parent="HBoxContainer"] [node name="ControlPrefab" type="ColorRect" parent="HBoxContainer"]
visible = false visible = false
margin_left = 677.0 layout_mode = 2
margin_right = 697.0 color = Color(0.129412, 0.121569, 0.121569, 1)
margin_bottom = 20.0
rect_min_size = Vector2( 20, 20 )
color = Color( 0.129412, 0.121569, 0.121569, 1 )
[node name="Buttons" type="VBoxContainer" parent="HBoxContainer/ControlPrefab"] [node name="Buttons" type="VBoxContainer" parent="HBoxContainer/ControlPrefab"]
layout_mode = 0
anchor_left = 0.5 anchor_left = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
margin_left = -65.5 offset_left = -65.5
margin_top = -22.0 offset_top = -22.0
margin_right = 65.5 offset_right = 65.5
margin_bottom = 22.0 offset_bottom = 22.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Rename" type="Button" parent="HBoxContainer/ControlPrefab/Buttons"] [node name="Rename" type="Button" parent="HBoxContainer/ControlPrefab/Buttons"]
margin_right = 131.0 layout_mode = 2
margin_bottom = 20.0
text = "Rename" text = "Rename"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Remove" type="Button" parent="HBoxContainer/ControlPrefab/Buttons"] [node name="Remove" type="Button" parent="HBoxContainer/ControlPrefab/Buttons"]
margin_top = 24.0 layout_mode = 2
margin_right = 131.0
margin_bottom = 44.0
text = "REMOVE" text = "REMOVE"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="DockableContainers" type="HBoxContainer" parent="."] [node name="DockableContainers" type="HBoxContainer" parent="."]
margin_top = 24.0 layout_mode = 2
margin_right = 1024.0
margin_bottom = 600.0
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
[node name="DockableContainer" type="Container" parent="DockableContainers"] [node name="DockableContainer" type="Container" parent="DockableContainers"]
margin_right = 483.0 layout_mode = 2
margin_bottom = 576.0
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
script = ExtResource( 1 ) script = ExtResource("1")
__meta__ = { layout = SubResource("Resource_ybwqe")
"_edit_use_anchors_": false
}
layout = SubResource( 2 )
[node name="Control0" type="ColorRect" parent="DockableContainers/DockableContainer"] [node name="Control0" type="ColorRect" parent="DockableContainers/DockableContainer"]
margin_left = 4.0 layout_mode = 2
margin_top = 32.0
margin_right = 479.0
margin_bottom = 278.0
rect_min_size = Vector2( 64, 64 )
[node name="Control1" type="ColorRect" parent="DockableContainers/DockableContainer"] [node name="Control1" type="ColorRect" parent="DockableContainers/DockableContainer"]
margin_left = 4.0 layout_mode = 2
margin_top = 326.0 color = Color(0.141176, 0.0745098, 0.603922, 1)
margin_right = 479.0
margin_bottom = 572.0
rect_min_size = Vector2( 128, 128 )
color = Color( 0.141176, 0.0745098, 0.603922, 1 )
[node name="Control2" type="ColorRect" parent="DockableContainers/DockableContainer"] [node name="Control2" type="ColorRect" parent="DockableContainers/DockableContainer"]
visible = false visible = false
margin_top = 294.0 layout_mode = 2
margin_right = 64.0 color = Color(0.533333, 0.380392, 0.380392, 1)
margin_bottom = 358.0
rect_min_size = Vector2( 64, 64 )
color = Color( 0.533333, 0.380392, 0.380392, 1 )
[node name="Separator" type="ColorRect" parent="DockableContainers"] [node name="Separator" type="ColorRect" parent="DockableContainers"]
margin_left = 487.0 custom_minimum_size = Vector2(50, 0)
margin_right = 537.0 layout_mode = 2
margin_bottom = 576.0 color = Color(0, 0, 0, 1)
rect_min_size = Vector2( 50, 0 )
color = Color( 0, 0, 0, 1 )
[node name="DockableContainer2" type="Container" parent="DockableContainers"] [node name="DockableContainer2" type="Container" parent="DockableContainers"]
margin_left = 541.0 layout_mode = 2
margin_right = 1024.0
margin_bottom = 576.0
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
script = ExtResource( 1 ) script = ExtResource("1")
__meta__ = { layout = SubResource("Resource_xhxpg")
"_edit_use_anchors_": false
}
layout = SubResource( 4 )
[node name="Control3" type="ColorRect" parent="DockableContainers/DockableContainer2"] [node name="Control3" type="ColorRect" parent="DockableContainers/DockableContainer2"]
margin_left = 4.0 layout_mode = 2
margin_top = 32.0 color = Color(0, 1, 0.905882, 1)
margin_right = 231.5
margin_bottom = 152.0
rect_min_size = Vector2( 64, 64 )
color = Color( 0, 1, 0.905882, 1 )
[node name="Control4" type="ColorRect" parent="DockableContainers/DockableContainer2"] [node name="Control4" type="ColorRect" parent="DockableContainers/DockableContainer2"]
margin_left = 4.0 layout_mode = 2
margin_top = 200.0 color = Color(0, 0.698039, 0.0588235, 1)
margin_right = 231.5
margin_bottom = 572.0
rect_min_size = Vector2( 128, 128 )
color = Color( 0, 0.698039, 0.0588235, 1 )
[node name="Control5" type="ColorRect" parent="DockableContainers/DockableContainer2"] [node name="Control5" type="ColorRect" parent="DockableContainers/DockableContainer2"]
margin_left = 251.5 layout_mode = 2
margin_top = 32.0 color = Color(1, 0.937255, 0, 1)
margin_right = 479.0
margin_bottom = 572.0
rect_min_size = Vector2( 64, 64 )
color = Color( 1, 0.937255, 0, 1 )
[connection signal="pressed" from="HBoxContainer/AddControlButton" to="." method="_on_add_pressed"] [connection signal="pressed" from="HBoxContainer/AddControlButton" to="." method="_on_add_pressed"]
[connection signal="pressed" from="HBoxContainer/SaveLayoutButton" to="." method="_on_save_pressed"] [connection signal="pressed" from="HBoxContainer/SaveLayoutButton" to="." method="_on_save_pressed"]

View file

@ -1,53 +1,49 @@
tool @tool
extends Control extends Control
const Layout = preload("layout.gd") const SPLIT_THEME_CLASS: PackedStringArray = [
"HSplitContainer", # SPLIT_THEME_CLASS[DockableLayoutSplit.Direction.HORIZONTAL]
const SPLIT_THEME_CLASS = [ "VSplitContainer", # SPLIT_THEME_CLASS[DockableLayoutSplit.Direction.VERTICAL]
"HSplitContainer", # SPLIT_THEME_CLASS[LayoutSplit.Direction.HORIZONTAL]
"VSplitContainer", # SPLIT_THEME_CLASS[LayoutSplit.Direction.VERTICAL]
] ]
const SPLIT_MOUSE_CURSOR_SHAPE = [ const SPLIT_MOUSE_CURSOR_SHAPE: Array[Control.CursorShape] = [
Control.CURSOR_HSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[LayoutSplit.Direction.HORIZONTAL] Control.CURSOR_HSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[DockableLayoutSplit.Direction.HORIZONTAL]
Control.CURSOR_VSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[LayoutSplit.Direction.VERTICAL] Control.CURSOR_VSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[DockableLayoutSplit.Direction.VERTICAL]
] ]
var layout_split: Layout.LayoutSplit var layout_split: DockableLayoutSplit
var first_minimum_size: Vector2 var first_minimum_size: Vector2
var second_minimum_size: Vector2 var second_minimum_size: Vector2
var _parent_rect var _parent_rect: Rect2
var _mouse_hovering = false var _mouse_hovering := false
var _dragging = false var _dragging := false
func _draw() -> void: func _draw() -> void:
var theme_class = SPLIT_THEME_CLASS[layout_split.direction] var theme_class := SPLIT_THEME_CLASS[layout_split.direction]
var icon = get_icon("grabber", theme_class) var icon := get_theme_icon("grabber", theme_class)
var autohide = bool(get_constant("autohide", theme_class)) var autohide := bool(get_theme_constant("autohide", theme_class))
if not icon or (autohide and not _mouse_hovering): if not icon or (autohide and not _mouse_hovering):
return return
draw_texture(icon, (rect_size - icon.get_size()) * 0.5) draw_texture(icon, (size - icon.get_size()) * 0.5)
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == BUTTON_LEFT: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
_dragging = event.is_pressed() _dragging = event.is_pressed()
if event.doubleclick: if event.double_click:
layout_split.percent = 0.5 layout_split.percent = 0.5
elif _dragging and event is InputEventMouseMotion: elif _dragging and event is InputEventMouseMotion:
var mouse_in_parent = get_parent_control().get_local_mouse_position() var mouse_in_parent := get_parent_control().get_local_mouse_position()
if layout_split.is_horizontal(): if layout_split.is_horizontal():
layout_split.percent = ( layout_split.percent = (
(mouse_in_parent.x - _parent_rect.position.x) (mouse_in_parent.x - _parent_rect.position.x) / _parent_rect.size.x
/ _parent_rect.size.x
) )
else: else:
layout_split.percent = ( layout_split.percent = (
(mouse_in_parent.y - _parent_rect.position.y) (mouse_in_parent.y - _parent_rect.position.y) / _parent_rect.size.y
/ _parent_rect.size.y
) )
@ -55,13 +51,13 @@ func _notification(what: int) -> void:
if what == NOTIFICATION_MOUSE_ENTER: if what == NOTIFICATION_MOUSE_ENTER:
_mouse_hovering = true _mouse_hovering = true
set_split_cursor(true) set_split_cursor(true)
if bool(get_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])): if bool(get_theme_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
update() queue_redraw()
elif what == NOTIFICATION_MOUSE_EXIT: elif what == NOTIFICATION_MOUSE_EXIT:
_mouse_hovering = false _mouse_hovering = false
set_split_cursor(false) set_split_cursor(false)
if bool(get_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])): if bool(get_theme_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
update() queue_redraw()
elif what == NOTIFICATION_FOCUS_EXIT: elif what == NOTIFICATION_FOCUS_EXIT:
_dragging = false _dragging = false
@ -69,15 +65,15 @@ func _notification(what: int) -> void:
func get_layout_minimum_size() -> Vector2: func get_layout_minimum_size() -> Vector2:
if not layout_split: if not layout_split:
return Vector2.ZERO return Vector2.ZERO
var separation = get_constant("separation", SPLIT_THEME_CLASS[layout_split.direction]) var separation := get_theme_constant("separation", SPLIT_THEME_CLASS[layout_split.direction])
if layout_split.is_horizontal(): if layout_split.is_horizontal():
return Vector2( return Vector2(
first_minimum_size.x + separation + second_minimum_size.x, first_minimum_size.x + separation + second_minimum_size.x,
max(first_minimum_size.y, second_minimum_size.y) maxf(first_minimum_size.y, second_minimum_size.y)
) )
else: else:
return Vector2( return Vector2(
max(first_minimum_size.x, second_minimum_size.x), maxf(first_minimum_size.x, second_minimum_size.x),
first_minimum_size.y + separation + second_minimum_size.y first_minimum_size.y + separation + second_minimum_size.y
) )
@ -91,41 +87,34 @@ func set_split_cursor(value: bool) -> void:
func get_split_rects(rect: Rect2) -> Dictionary: func get_split_rects(rect: Rect2) -> Dictionary:
_parent_rect = rect _parent_rect = rect
var separation = get_constant("separation", SPLIT_THEME_CLASS[layout_split.direction]) var separation := get_theme_constant("separation", SPLIT_THEME_CLASS[layout_split.direction])
var origin = rect.position var origin := rect.position
var size = rect.size var percent := layout_split.percent
var percent = layout_split.percent
if layout_split.is_horizontal(): if layout_split.is_horizontal():
var first_width = max((size.x - separation) * percent, first_minimum_size.x) var split_offset := clampf(
var split_offset = clamp( rect.size.x * percent - separation * 0.5,
size.x * percent - separation * 0.5,
first_minimum_size.x, first_minimum_size.x,
size.x - second_minimum_size.x - separation rect.size.x - second_minimum_size.x - separation
) )
var second_width = size.x - split_offset - separation var second_width := rect.size.x - split_offset - separation
return { return {
"first": Rect2(origin.x, origin.y, split_offset, size.y), "first": Rect2(origin.x, origin.y, split_offset, rect.size.y),
"self": Rect2(origin.x + split_offset, origin.y, separation, size.y), "self": Rect2(origin.x + split_offset, origin.y, separation, rect.size.y),
"second": Rect2(origin.x + split_offset + separation, origin.y, second_width, size.y), "second":
Rect2(origin.x + split_offset + separation, origin.y, second_width, rect.size.y),
} }
else: else:
var first_height = max((size.y - separation) * percent, first_minimum_size.y) var split_offset := clampf(
var split_offset = clamp( rect.size.y * percent - separation * 0.5,
size.y * percent - separation * 0.5,
first_minimum_size.y, first_minimum_size.y,
size.y - second_minimum_size.y - separation rect.size.y - second_minimum_size.y - separation
) )
var second_height = size.y - split_offset - separation var second_height := rect.size.y - split_offset - separation
return { return {
"first": Rect2(origin.x, origin.y, size.x, split_offset), "first": Rect2(origin.x, origin.y, rect.size.x, split_offset),
"self": Rect2(origin.x, origin.y + split_offset, size.x, separation), "self": Rect2(origin.x, origin.y + split_offset, rect.size.x, separation),
"second": Rect2(origin.x, origin.y + split_offset + separation, size.x, second_height), "second":
Rect2(origin.x, origin.y + split_offset + separation, rect.size.x, second_height),
} }
static func get_separation_with_control(control: Control) -> Vector2:
var hseparation = control.get_constant("separation", "HSplitContainer")
var vseparation = control.get_constant("separation", "VSplitContainer")
return Vector2(hseparation, vseparation)

View file

@ -1,59 +1,53 @@
extends Reference extends RefCounted
var _shader: Shader var _shader: Shader
func get_indexed_datas(image: Image, colors: Array) -> PoolByteArray: func get_indexed_datas(image: Image, colors: Array) -> PackedByteArray:
_shader = preload("./lookup_color.shader") _shader = preload("./lookup_color.gdshader")
return _convert(image, colors) return _convert(image, colors)
func get_similar_indexed_datas(image: Image, colors: Array) -> PoolByteArray: func get_similar_indexed_datas(image: Image, colors: Array) -> PackedByteArray:
_shader = preload("./lookup_similar.shader") _shader = preload("./lookup_similar.gdshader")
return _convert(image, colors) return _convert(image, colors)
func _convert(image: Image, colors: Array) -> PoolByteArray: func _convert(image: Image, colors: Array) -> PackedByteArray:
var vp = VisualServer.viewport_create() var vp := RenderingServer.viewport_create()
var canvas = VisualServer.canvas_create() var canvas := RenderingServer.canvas_create()
VisualServer.viewport_attach_canvas(vp, canvas) RenderingServer.viewport_attach_canvas(vp, canvas)
VisualServer.viewport_set_size(vp, image.get_width(), image.get_height()) RenderingServer.viewport_set_size(vp, image.get_width(), image.get_height())
VisualServer.viewport_set_disable_3d(vp, true) RenderingServer.viewport_set_disable_3d(vp, true)
VisualServer.viewport_set_usage(vp, VisualServer.VIEWPORT_USAGE_2D) RenderingServer.viewport_set_active(vp, true)
VisualServer.viewport_set_hdr(vp, true)
VisualServer.viewport_set_active(vp, true)
var ci_rid = VisualServer.canvas_item_create() var ci_rid := RenderingServer.canvas_item_create()
VisualServer.viewport_set_canvas_transform(vp, canvas, Transform()) RenderingServer.viewport_set_canvas_transform(vp, canvas, Transform3D())
VisualServer.canvas_item_set_parent(ci_rid, canvas) RenderingServer.canvas_item_set_parent(ci_rid, canvas)
var texture = ImageTexture.new() var texture := ImageTexture.create_from_image(image)
texture.create_from_image(image) RenderingServer.canvas_item_add_texture_rect(
VisualServer.canvas_item_add_texture_rect( ci_rid, Rect2(Vector2.ZERO, image.get_size()), texture
ci_rid, Rect2(Vector2(0, 0), image.get_size()), texture
) )
var mat_rid = VisualServer.material_create() var mat_rid := RenderingServer.material_create()
VisualServer.material_set_shader(mat_rid, _shader.get_rid()) RenderingServer.material_set_shader(mat_rid, _shader.get_rid())
var lut = Image.new() var lut := Image.create(256, 1, false, Image.FORMAT_RGB8)
lut.create(256, 1, false, Image.FORMAT_RGB8)
lut.fill(Color8(colors[0][0], colors[0][1], colors[0][2])) lut.fill(Color8(colors[0][0], colors[0][1], colors[0][2]))
lut.lock()
for i in colors.size(): for i in colors.size():
lut.set_pixel(i, 0, Color8(colors[i][0], colors[i][1], colors[i][2])) lut.set_pixel(i, 0, Color8(colors[i][0], colors[i][1], colors[i][2]))
var lut_tex = ImageTexture.new() var lut_tex := ImageTexture.create_from_image(lut)
lut_tex.create_from_image(lut) # Not sure why putting lut_tex is an array is needed, but without it, it doesn't work
VisualServer.material_set_param(mat_rid, "lut", lut_tex) RenderingServer.material_set_param(mat_rid, "lut", [lut_tex])
VisualServer.canvas_item_set_material(ci_rid, mat_rid) RenderingServer.canvas_item_set_material(ci_rid, mat_rid)
VisualServer.viewport_set_update_mode(vp, VisualServer.VIEWPORT_UPDATE_ONCE) RenderingServer.viewport_set_update_mode(vp, RenderingServer.VIEWPORT_UPDATE_ONCE)
VisualServer.viewport_set_vflip(vp, true) RenderingServer.force_draw(false)
VisualServer.force_draw(false) image = RenderingServer.texture_2d_get(RenderingServer.viewport_get_texture(vp))
image = VisualServer.texture_get_data(VisualServer.viewport_get_texture(vp))
VisualServer.free_rid(vp) RenderingServer.free_rid(vp)
VisualServer.free_rid(canvas) RenderingServer.free_rid(canvas)
VisualServer.free_rid(ci_rid) RenderingServer.free_rid(ci_rid)
VisualServer.free_rid(mat_rid) RenderingServer.free_rid(mat_rid)
image.convert(Image.FORMAT_R8) image.convert(Image.FORMAT_R8)
return image.get_data() return image.get_data()

View file

@ -1,16 +1,16 @@
extends Reference extends RefCounted
enum Error { OK = 0, EMPTY_IMAGE = 1, BAD_IMAGE_FORMAT = 2 } enum Error { OK = 0, EMPTY_IMAGE = 1, BAD_IMAGE_FORMAT = 2 }
var little_endian = preload("./little_endian.gd").new() var little_endian := preload("./little_endian.gd").new()
var lzw = preload("./gif-lzw/lzw.gd").new() var lzw := preload("./gif-lzw/lzw.gd").new()
var converter = preload("./converter.gd") var converter := preload("./converter.gd")
var last_color_table := [] var last_color_table := []
var last_transparency_index := -1 var last_transparency_index := -1
# File data and Header # File data and Header
var data := PoolByteArray([]) var data := PackedByteArray([])
func _init(_width: int, _height: int): func _init(_width: int, _height: int):
@ -19,12 +19,12 @@ func _init(_width: int, _height: int):
add_application_ext("NETSCAPE", "2.0", [1, 0, 0]) add_application_ext("NETSCAPE", "2.0", [1, 0, 0])
func export_file_data() -> PoolByteArray: func export_file_data() -> PackedByteArray:
return data + PoolByteArray([0x3b]) return data + PackedByteArray([0x3b])
func add_header() -> void: func add_header() -> void:
data += "GIF".to_ascii() + "89a".to_ascii() data += "GIF".to_ascii_buffer() + "89a".to_ascii_buffer()
func add_logical_screen_descriptor(width: int, height: int) -> void: func add_logical_screen_descriptor(width: int, height: int) -> void:
@ -53,18 +53,17 @@ func add_application_ext(app_iden: String, app_auth_code: String, _data: Array)
data.append(extension_introducer) data.append(extension_introducer)
data.append(extension_label) data.append(extension_label)
data.append(block_size) data.append(block_size)
data += app_iden.to_ascii() data += app_iden.to_ascii_buffer()
data += app_auth_code.to_ascii() data += app_auth_code.to_ascii_buffer()
data.append(_data.size()) data.append(_data.size())
data += PoolByteArray(_data) data += PackedByteArray(_data)
data.append(0) data.append(0)
# finds the image color table. Stops if the size gets larger than 256. # finds the image color table. Stops if the size gets larger than 256.
func find_color_table(image: Image) -> Dictionary: func find_color_table(image: Image) -> Dictionary:
image.lock()
var result: Dictionary = {} var result: Dictionary = {}
var image_data: PoolByteArray = image.get_data() var image_data: PackedByteArray = image.get_data()
for i in range(0, image_data.size(), 4): for i in range(0, image_data.size(), 4):
var color: Array = [ var color: Array = [
@ -77,8 +76,6 @@ func find_color_table(image: Image) -> Dictionary:
result[color] = result.size() result[color] = result.size()
if result.size() > 256: if result.size() > 256:
break break
image.unlock()
return result return result
@ -89,10 +86,11 @@ func find_transparency_color_index(color_table: Dictionary) -> int:
return -1 return -1
func colors_to_codes(img: Image, col_palette: Dictionary, transp_color_index: int) -> PoolByteArray: func colors_to_codes(
img.lock() img: Image, col_palette: Dictionary, transp_color_index: int
var image_data: PoolByteArray = img.get_data() ) -> PackedByteArray:
var result: PoolByteArray = PoolByteArray([]) var image_data: PackedByteArray = img.get_data()
var result: PackedByteArray = PackedByteArray([])
for i in range(0, image_data.size(), 4): for i in range(0, image_data.size(), 4):
var color: Array = [image_data[i], image_data[i + 1], image_data[i + 2], image_data[i + 3]] var color: Array = [image_data[i], image_data[i + 1], image_data[i + 2], image_data[i + 3]]
@ -106,7 +104,6 @@ func colors_to_codes(img: Image, col_palette: Dictionary, transp_color_index: in
result.append(0) result.append(0)
push_warning("colors_to_codes: color not found! [%d, %d, %d, %d]" % color) push_warning("colors_to_codes: color not found! [%d, %d, %d, %d]" % color)
img.unlock()
return result return result
@ -120,11 +117,11 @@ func make_proper_size(color_table: Array) -> Array:
func calc_delay_time(frame_delay: float) -> int: func calc_delay_time(frame_delay: float) -> int:
return int(ceil(frame_delay / 0.01)) return int(ceili(frame_delay / 0.01))
func color_table_to_indexes(colors: Array) -> PoolByteArray: func color_table_to_indexes(colors: Array) -> PackedByteArray:
var result: PoolByteArray = PoolByteArray([]) var result: PackedByteArray = PackedByteArray([])
for i in range(colors.size()): for i in range(colors.size()):
result.append(i) result.append(i)
return result return result
@ -141,13 +138,13 @@ func add_frame(image: Image, frame_delay: float, quantizator: Script) -> int:
var found_color_table: Dictionary = find_color_table(image) var found_color_table: Dictionary = find_color_table(image)
var image_converted_to_codes: PoolByteArray var image_converted_to_codes: PackedByteArray
var transparency_color_index: int = -1 var transparency_color_index: int = -1
var color_table: Array var color_table: Array
if found_color_table.size() <= 256: # we don't need to quantize the image. if found_color_table.size() <= 256: # we don't need to quantize the image.
# try to find transparency color index. # try to find transparency color index.
transparency_color_index = find_transparency_color_index(found_color_table) transparency_color_index = find_transparency_color_index(found_color_table)
# if didn't found transparency color index but there is atleast one # if didn't find transparency color index but there is at least one
# place for this color then add it artificially. # place for this color then add it artificially.
if transparency_color_index == -1 and found_color_table.size() <= 255: if transparency_color_index == -1 and found_color_table.size() <= 255:
found_color_table[[0, 0, 0, 0]] = found_color_table.size() found_color_table[[0, 0, 0, 0]] = found_color_table.size()
@ -172,7 +169,7 @@ func add_frame(image: Image, frame_delay: float, quantizator: Script) -> int:
var compressed_image_result: Array = lzw.compress_lzw( var compressed_image_result: Array = lzw.compress_lzw(
image_converted_to_codes, color_table_indexes image_converted_to_codes, color_table_indexes
) )
var compressed_image_data: PoolByteArray = compressed_image_result[0] var compressed_image_data: PackedByteArray = compressed_image_result[0]
var lzw_min_code_size: int = compressed_image_result[1] var lzw_min_code_size: int = compressed_image_result[1]
add_graphic_constrol_ext(delay_time, transparency_color_index) add_graphic_constrol_ext(delay_time, transparency_color_index)
@ -183,7 +180,7 @@ func add_frame(image: Image, frame_delay: float, quantizator: Script) -> int:
return Error.OK return Error.OK
# adds frame with last color informations ## Adds frame with last color information
func add_frame_with_lci(image: Image, frame_delay: float) -> int: func add_frame_with_lci(image: Image, frame_delay: float) -> int:
# check if image is of good format # check if image is of good format
if image.get_format() != Image.FORMAT_RGBA8: if image.get_format() != Image.FORMAT_RGBA8:
@ -193,7 +190,7 @@ func add_frame_with_lci(image: Image, frame_delay: float) -> int:
if image.is_empty(): if image.is_empty():
return Error.EMPTY_IMAGE return Error.EMPTY_IMAGE
var image_converted_to_codes: PoolByteArray = converter.new().get_similar_indexed_datas( var image_converted_to_codes: PackedByteArray = converter.new().get_similar_indexed_datas(
image, last_color_table image, last_color_table
) )
@ -201,7 +198,7 @@ func add_frame_with_lci(image: Image, frame_delay: float) -> int:
var compressed_image_result: Array = lzw.compress_lzw( var compressed_image_result: Array = lzw.compress_lzw(
image_converted_to_codes, color_table_indexes image_converted_to_codes, color_table_indexes
) )
var compressed_image_data: PoolByteArray = compressed_image_result[0] var compressed_image_data: PackedByteArray = compressed_image_result[0]
var lzw_min_code_size: int = compressed_image_result[1] var lzw_min_code_size: int = compressed_image_result[1]
var delay_time := calc_delay_time(frame_delay) var delay_time := calc_delay_time(frame_delay)
@ -258,38 +255,34 @@ func color_table_bit_size(color_table: Array) -> int:
func add_local_color_table(color_table: Array) -> void: func add_local_color_table(color_table: Array) -> void:
for color in color_table: for color in color_table:
data.append_array([color[0], color[1], color[2]]) data.append(color[0])
data.append(color[1])
data.append(color[2])
var size := color_table_bit_size(color_table) var size := color_table_bit_size(color_table)
var proper_size := int(pow(2, size + 1)) var proper_size := int(pow(2, size + 1))
if color_table.size() != proper_size: if color_table.size() != proper_size:
for i in range(proper_size - color_table.size()): for i in range(proper_size - color_table.size()):
data.append_array([0, 0, 0]) data += PackedByteArray([0, 0, 0])
func add_image_data_block(lzw_min_code_size: int, frame_data: PoolByteArray) -> void: func add_image_data_block(lzw_min_code_size: int, _data: PackedByteArray) -> void:
var max_block_size = 254
data.append(lzw_min_code_size) data.append(lzw_min_code_size)
# the amount of blocks which will be stored
# ceiled because the last block doesn't have to be exactly max_block_size
var block_count = ceil(frame_data.size() / float(max_block_size))
for i in range(block_count): var block_size_index: int = 0
var start_block_index = i * max_block_size var i: int = 0
var end_block_index = (i * max_block_size) + max_block_size - 1 var data_index: int = 0
# final block can be smaller than max block size while data_index < _data.size():
end_block_index = ( if i == 0:
end_block_index data.append(0)
if end_block_index < frame_data.size() - 1 block_size_index = data.size() - 1
else frame_data.size() - 1 data.append(_data[data_index])
) data[block_size_index] += 1
var block_size = end_block_index - start_block_index + 1 data_index += 1
var block = frame_data.subarray(start_block_index, end_block_index) i += 1
if i == 254:
i = 0
# store block size and it's data if not _data.is_empty():
data.append(block_size)
data.append_array(block)
if not frame_data.empty():
data.append(0) data.append(0)

View file

@ -1,11 +1,11 @@
extends Reference extends RefCounted
class LSBLZWBitPacker: class LSBLZWBitPacker:
var bit_index: int = 0 var bit_index: int = 0
var stream: int = 0 var stream: int = 0
var chunks: PoolByteArray = PoolByteArray([]) var chunks: PackedByteArray = PackedByteArray([])
func put_byte(): func put_byte():
chunks.append(stream & 0xff) chunks.append(stream & 0xff)
@ -20,7 +20,7 @@ class LSBLZWBitPacker:
while bit_index >= 8: while bit_index >= 8:
self.put_byte() self.put_byte()
func pack() -> PoolByteArray: func pack() -> PackedByteArray:
if bit_index != 0: if bit_index != 0:
self.put_byte() self.put_byte()
return chunks return chunks
@ -28,4 +28,4 @@ class LSBLZWBitPacker:
func reset() -> void: func reset() -> void:
bit_index = 0 bit_index = 0
stream = 0 stream = 0
chunks = PoolByteArray([]) chunks = PackedByteArray([])

View file

@ -1,13 +1,13 @@
extends Reference extends RefCounted
class LSBLZWBitUnpacker: class LSBLZWBitUnpacker:
var chunk_stream: PoolByteArray var chunk_stream: PackedByteArray
var bit_index: int = 0 var bit_index: int = 0
var byte: int var byte: int
var byte_index: int = 0 var byte_index: int = 0
func _init(_chunk_stream: PoolByteArray): func _init(_chunk_stream: PackedByteArray):
chunk_stream = _chunk_stream chunk_stream = _chunk_stream
self.get_byte() self.get_byte()

View file

@ -1,38 +1,84 @@
extends Reference extends RefCounted
var lsbbitpacker = preload("./lsbbitpacker.gd") var lsbbitpacker := preload("./lsbbitpacker.gd")
var lsbbitunpacker = preload("./lsbbitunpacker.gd") var lsbbitunpacker := preload("./lsbbitunpacker.gd")
var code_table := {}
var entries_counter := 0
func get_bit_length(value: int): class CodeEntry:
# bitwise or on value does ensure that the function works with value 0 var sequence: PackedByteArray
# long number at the end is log(2.0) var raw_array: Array
return ceil(log(value | 0x1 + 1) / 0.6931471805599453)
func _init(_sequence):
raw_array = _sequence
sequence = _sequence
func add(other: CodeEntry) -> CodeEntry:
return CodeEntry.new(self.raw_array + other.raw_array)
func _to_string():
var result: String = ""
for element in self.sequence:
result += str(element) + ", "
return result.substr(0, result.length() - 2)
func initialize_color_code_table(colors: PoolByteArray) -> void: class CodeTable:
code_table.clear() var entries: Dictionary = {}
entries_counter = 0 var counter: int = 0
var lookup: Dictionary = {}
func add(entry) -> int:
self.entries[self.counter] = entry
self.lookup[entry.raw_array] = self.counter
counter += 1
return counter
func find(entry) -> int:
return self.lookup.get(entry.raw_array, -1)
func has(entry) -> bool:
return self.find(entry) != -1
func get_entry(index: int) -> CodeEntry:
return self.entries.get(index, null)
func _to_string() -> String:
var result: String = "CodeTable:\n"
for id in self.entries:
result += str(id) + ": " + self.entries[id].to_string() + "\n"
result += "Counter: " + str(self.counter) + "\n"
return result
func log2(value: float) -> float:
return log(value) / log(2.0)
func get_bits_number_for(value: int) -> int:
if value == 0:
return 1
return int(ceili(log2(value + 1)))
func initialize_color_code_table(colors: PackedByteArray) -> CodeTable:
var result_code_table: CodeTable = CodeTable.new()
for color_id in colors: for color_id in colors:
# warning-ignore:return_value_discarded # warning-ignore:return_value_discarded
code_table[PoolByteArray([color_id])] = entries_counter result_code_table.add(CodeEntry.new([color_id]))
entries_counter += 1
# move counter to the first available compression code index # move counter to the first available compression code index
var last_color_index: int = colors.size() - 1 var last_color_index: int = colors.size() - 1
var clear_code_index: int = pow(2, get_bit_length(last_color_index)) var clear_code_index: int = pow(2, get_bits_number_for(last_color_index))
entries_counter = clear_code_index + 2 result_code_table.counter = clear_code_index + 2
return result_code_table
# compression and decompression done with source: # compression and decompression done with source:
# http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp # http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp
func compress_lzw(index_stream: PoolByteArray, colors: PoolByteArray) -> Array: func compress_lzw(image: PackedByteArray, colors: PackedByteArray) -> Array:
# Initialize code table # Initialize code table
initialize_color_code_table(colors) var code_table: CodeTable = initialize_color_code_table(colors)
# Clear Code index is 2**<code size> # Clear Code index is 2**<code size>
# <code size> is the amount of bits needed to write down all colors # <code size> is the amount of bits needed to write down all colors
# from color table. We use last color index because we can write # from color table. We use last color index because we can write
@ -40,63 +86,127 @@ func compress_lzw(index_stream: PoolByteArray, colors: PoolByteArray) -> Array:
# Number 15 is in binary 0b1111, so we'll need 4 bits to write all # Number 15 is in binary 0b1111, so we'll need 4 bits to write all
# colors down. # colors down.
var last_color_index: int = colors.size() - 1 var last_color_index: int = colors.size() - 1
var clear_code_index: int = pow(2, get_bit_length(last_color_index)) var clear_code_index: int = pow(2, get_bits_number_for(last_color_index))
var current_code_size: int = get_bit_length(clear_code_index) var index_stream: PackedByteArray = image
var binary_code_stream = lsbbitpacker.LSBLZWBitPacker.new() var current_code_size: int = get_bits_number_for(clear_code_index)
var binary_code_stream := lsbbitpacker.LSBLZWBitPacker.new()
# initialize with Clear Code # initialize with Clear Code
binary_code_stream.write_bits(clear_code_index, current_code_size) binary_code_stream.write_bits(clear_code_index, current_code_size)
# Read first index from index stream. # Read first index from index stream.
var index_buffer := PoolByteArray([index_stream[0]]) var index_buffer := CodeEntry.new([index_stream[0]])
var data_index: int = 1 var data_index: int = 1
# <LOOP POINT> # <LOOP POINT>
while data_index < index_stream.size(): while data_index < index_stream.size():
# Get the next index from the index stream. # Get the next index from the index stream.
var k := index_stream[data_index] var k := CodeEntry.new([index_stream[data_index]])
data_index += 1 data_index += 1
# Is index buffer + k in our code table? # Is index buffer + k in our code table?
var new_index_buffer := PoolByteArray(index_buffer) var new_index_buffer := index_buffer.add(k)
new_index_buffer.push_back(k)
if code_table.has(new_index_buffer): # if YES if code_table.has(new_index_buffer): # if YES
# Add k to the end of the index buffer # Add k to the end of the index buffer
index_buffer = new_index_buffer index_buffer = new_index_buffer
else: # if NO else: # if NO
# Add a row for index buffer + k into our code table # Add a row for index buffer + k into our code table
binary_code_stream.write_bits(code_table.get(index_buffer, -1), current_code_size) binary_code_stream.write_bits(code_table.find(index_buffer), current_code_size)
# We don't want to add new code to code table if we've exceeded 4095 # We don't want to add new code to code table if we've exceeded 4095
# index. # index.
var last_entry_index: int = entries_counter - 1 var last_entry_index: int = code_table.counter - 1
if last_entry_index != 4095: if last_entry_index != 4095:
# Output the code for just the index buffer to our code stream # Output the code for just the index buffer to our code stream
# warning-ignore:return_value_discarded # warning-ignore:return_value_discarded
code_table[new_index_buffer] = entries_counter code_table.add(new_index_buffer)
entries_counter += 1
else: else:
# if we exceeded 4095 index (code table is full), we should # if we exceeded 4095 index (code table is full), we should
# output Clear Code and reset everything. # output Clear Code and reset everything.
binary_code_stream.write_bits(clear_code_index, current_code_size) binary_code_stream.write_bits(clear_code_index, current_code_size)
initialize_color_code_table(colors) code_table = initialize_color_code_table(colors)
# get_bits_number_for(clear_code_index) is the same as # get_bits_number_for(clear_code_index) is the same as
# LZW code size + 1 # LZW code size + 1
current_code_size = get_bit_length(clear_code_index) current_code_size = get_bits_number_for(clear_code_index)
# Detect when you have to save new codes in bigger bits boxes # Detect when you have to save new codes in bigger bits boxes
# change current code size when it happens because we want to save # change current code size when it happens because we want to save
# flexible code sized codes # flexible code sized codes
var new_code_size_candidate: int = get_bit_length(entries_counter - 1) var new_code_size_candidate: int = get_bits_number_for(code_table.counter - 1)
if new_code_size_candidate > current_code_size: if new_code_size_candidate > current_code_size:
current_code_size = new_code_size_candidate current_code_size = new_code_size_candidate
# Index buffer is set to k # Index buffer is set to k
index_buffer = PoolByteArray([k]) index_buffer = k
# Output code for contents of index buffer # Output code for contents of index buffer
binary_code_stream.write_bits(code_table.get(index_buffer, -1), current_code_size) binary_code_stream.write_bits(code_table.find(index_buffer), current_code_size)
# output end with End Of Information Code # output end with End Of Information Code
binary_code_stream.write_bits(clear_code_index + 1, current_code_size) binary_code_stream.write_bits(clear_code_index + 1, current_code_size)
var min_code_size: int = get_bit_length(clear_code_index) - 1 var min_code_size: int = get_bits_number_for(clear_code_index) - 1
return [binary_code_stream.pack(), min_code_size] return [binary_code_stream.pack(), min_code_size]
# gdlint: ignore=max-line-length
func decompress_lzw(
code_stream_data: PackedByteArray, min_code_size: int, colors: PackedByteArray
) -> PackedByteArray:
var code_table: CodeTable = initialize_color_code_table(colors)
var index_stream := PackedByteArray([])
var binary_code_stream = lsbbitunpacker.LSBLZWBitUnpacker.new(code_stream_data)
var current_code_size: int = min_code_size + 1
var clear_code_index: int = pow(2, min_code_size)
# CODE is an index of code table, {CODE} is sequence inside
# code table with index CODE. The same goes for PREVCODE.
# Remove first Clear Code from stream. We don't need it.
binary_code_stream.remove_bits(current_code_size)
# let CODE be the first code in the code stream
var code: int = binary_code_stream.read_bits(current_code_size)
# output {CODE} to index stream
index_stream.append_array(code_table.get_entry(code).sequence)
# set PREVCODE = CODE
var prevcode: int = code
# <LOOP POINT>
while true:
# let CODE be the next code in the code stream
code = binary_code_stream.read_bits(current_code_size)
# Detect Clear Code. When detected reset everything and get next code.
if code == clear_code_index:
code_table = initialize_color_code_table(colors)
current_code_size = min_code_size + 1
code = binary_code_stream.read_bits(current_code_size)
elif code == clear_code_index + 1: # Stop when detected EOI Code.
break
# is CODE in the code table?
var code_entry: CodeEntry = code_table.get_entry(code)
if code_entry != null: # if YES
# output {CODE} to index stream
index_stream.append_array(code_entry.sequence)
# let k be the first index in {CODE}
var k: CodeEntry = CodeEntry.new([code_entry.sequence[0]])
# warning-ignore:return_value_discarded
# add {PREVCODE} + k to the code table
code_table.add(code_table.get_entry(prevcode).add(k))
# set PREVCODE = CODE
prevcode = code
else: # if NO
# let k be the first index of {PREVCODE}
var prevcode_entry: CodeEntry = code_table.get_entry(prevcode)
var k: CodeEntry = CodeEntry.new([prevcode_entry.sequence[0]])
# output {PREVCODE} + k to index stream
index_stream.append_array(prevcode_entry.add(k).sequence)
# add {PREVCODE} + k to code table
# warning-ignore:return_value_discarded
code_table.add(prevcode_entry.add(k))
# set PREVCODE = CODE
prevcode = code
# Detect when we should increase current code size and increase it.
var new_code_size_candidate: int = get_bits_number_for(code_table.counter)
if new_code_size_candidate > current_code_size:
current_code_size = new_code_size_candidate
return index_stream

View file

@ -1,5 +1,5 @@
extends Reference extends RefCounted
func int_to_2bytes(value: int) -> PoolByteArray: func int_to_2bytes(value: int) -> PackedByteArray:
return PoolByteArray([value & 255, (value >> 8) & 255]) return PackedByteArray([value & 255, (value >> 8) & 255])

View file

@ -1,20 +1,20 @@
extends Reference extends RefCounted
var converter = preload("../converter.gd").new() var converter := preload("../converter.gd").new()
var transparency := false var transparency := false
func longest_axis(colors: Array) -> int: func longest_axis(colors: Array) -> int:
var start := [255, 255, 255] var start: PackedInt32Array = [255, 255, 255]
var end := [0, 0, 0] var end: PackedInt32Array = [0, 0, 0]
for color in colors: for color in colors:
for i in 3: for i in 3:
start[i] = min(color[i], start[i]) start[i] = mini(color[i], start[i])
end[i] = max(color[i], end[i]) end[i] = maxi(color[i], end[i])
var max_r = end[0] - start[0] var max_r := end[0] - start[0]
var max_g = end[1] - start[1] var max_g := end[1] - start[1]
var max_b = end[2] - start[2] var max_b := end[2] - start[2]
if max_r > max_g: if max_r > max_g:
if max_r > max_b: if max_r > max_b:
@ -73,16 +73,14 @@ func average_colors(buckets: Array) -> Dictionary:
func pixels_to_colors(image: Image) -> Array: func pixels_to_colors(image: Image) -> Array:
image.lock()
var result := [] var result := []
var data: PoolByteArray = image.get_data() var data: PackedByteArray = image.get_data()
for i in range(0, data.size(), 4): for i in range(0, data.size(), 4):
if data[i + 3] == 0: if data[i + 3] == 0:
transparency = true transparency = true
continue continue
result.append([data[i], data[i + 1], data[i + 2]]) result.append([data[i], data[i + 1], data[i + 2]])
image.unlock()
return result return result
@ -93,7 +91,7 @@ func remove_smallest_bucket(buckets: Array) -> Array:
for i in range(buckets.size()): for i in range(buckets.size()):
if buckets[i].size() < buckets[i_of_smallest_bucket].size(): if buckets[i].size() < buckets[i_of_smallest_bucket].size():
i_of_smallest_bucket = i i_of_smallest_bucket = i
buckets.remove(i_of_smallest_bucket) buckets.remove_at(i_of_smallest_bucket)
return buckets return buckets
@ -103,15 +101,15 @@ func remove_empty_buckets(buckets: Array) -> Array:
var i := buckets.find([]) var i := buckets.find([])
while i != -1: while i != -1:
buckets.remove(i) buckets.remove_at(i)
i = buckets.find([]) i = buckets.find([])
return buckets return buckets
# quantizes to gif ready codes ## Quantizes to gif ready codes
func quantize(image: Image) -> Array: func quantize(image: Image) -> Array:
var pixels = pixels_to_colors(image) var pixels := pixels_to_colors(image)
if pixels.size() == 0: if pixels.size() == 0:
return pixels return pixels
@ -158,6 +156,6 @@ func quantize(image: Image) -> Array:
if transparency: if transparency:
color_array = [[0, 0, 0]] + color_array color_array = [[0, 0, 0]] + color_array
var data: PoolByteArray = converter.get_similar_indexed_datas(image, color_array) var data: PackedByteArray = converter.get_similar_indexed_datas(image, color_array)
return [data, color_array, transparency] return [data, color_array, transparency]

View file

@ -0,0 +1,82 @@
extends RefCounted
var converter := preload("../converter.gd").new()
var transparency := false
func how_many_divisions(colors_count: int) -> int:
return int(ceili(pow(colors_count, 1.0 / 4.0)))
func generate_colors(colors_count: int) -> Array:
var divisions_count: int = how_many_divisions(colors_count)
var colors: Array = []
for a in range(divisions_count):
for b in range(divisions_count):
for g in range(divisions_count):
for r in range(divisions_count):
colors.append(
[
Vector3(
(255.0 / divisions_count) * r,
(255.0 / divisions_count) * g,
(255.0 / divisions_count) * b
),
(255.0 / divisions_count) * a
]
)
return colors
func find_nearest_color(palette_color: Vector3, image_data: PackedByteArray) -> Array:
var nearest_color = null
var nearest_alpha = null
for i in range(0, image_data.size(), 4):
var color := Vector3(image_data[i], image_data[i + 1], image_data[i + 2])
# detect transparency
if image_data[3] == 0:
transparency = true
if (
(nearest_color == null)
or (
palette_color.distance_squared_to(color)
< palette_color.distance_squared_to(nearest_color)
)
):
nearest_color = color
nearest_alpha = image_data[i + 3]
return [nearest_color, nearest_alpha]
## Moves every color from palette colors to the nearest found color in image
func enhance_colors(image: Image, palette_colors: Array) -> Array:
var data := image.get_data()
for i in range(palette_colors.size()):
var nearest_color := find_nearest_color(palette_colors[i][0], data)
palette_colors[i] = nearest_color
return palette_colors
func to_color_array(colors: Array) -> Array:
var result := []
for v in colors:
result.append([v[0].x, v[0].y, v[0].z])
return result
## Quantizes to gif ready codes
func quantize(image: Image) -> Array:
var colors: Array = generate_colors(256)
var tmp_image := Image.new()
tmp_image.copy_from(image)
tmp_image.resize(32, 32)
colors = enhance_colors(tmp_image, colors)
colors = to_color_array(colors)
var data: PackedByteArray = converter.get_similar_indexed_datas(image, colors)
return [data, colors, transparency]

View file

@ -1,21 +1,19 @@
extends Node extends Node
const TRANSLATIONS_PATH := "res://addons/keychain/translations"
const PROFILES_PATH := "user://shortcut_profiles" const PROFILES_PATH := "user://shortcut_profiles"
# Change these settings ## Change these settings
var profiles := [preload("profiles/default.tres")] var profiles: Array[ShortcutProfile] = [preload("profiles/default.tres")]
var selected_profile: ShortcutProfile = profiles[0] var selected_profile := profiles[0]
var profile_index := 0 var profile_index := 0
# Syntax: "action_name": InputAction.new("Action Display Name", "Group", true) ## Syntax: "action_name": InputAction.new("Action Display Name", "Group", true)
# Note that "action_name" must already exist in the Project's Input Map. ## Note that "action_name" must already exist in the Project's Input Map.
var actions := {} var actions := {}
# Syntax: "Group Name": InputGroup.new("Parent Group Name") ## Syntax: "Group Name": InputGroup.new("Parent Group Name")
var groups := {} var groups := {}
var ignore_actions := [] var ignore_actions := []
var ignore_ui_actions := true var ignore_ui_actions := true
var changeable_types := [true, true, true, true] var changeable_types := [true, true, true, true]
var multiple_menu_accelerators := false
var config_path := "user://cache.ini" var config_path := "user://cache.ini"
var config_file: ConfigFile var config_file: ConfigFile
@ -25,79 +23,11 @@ class InputAction:
var group := "" var group := ""
var global := true var global := true
func _init(_display_name := "", _group := "", _global := true) -> void: func _init(_display_name := "", _group := "", _global := true):
display_name = _display_name display_name = _display_name
group = _group group = _group
global = _global global = _global
func update_node(_action: String) -> void:
pass
func handle_input(_event: InputEvent, _action: String) -> bool:
return false
# This class is useful for the accelerators of PopupMenu items
# It's possible for PopupMenu items to have multiple shortcuts by using
# set_item_shortcut(), but we have no control over the accelerator text that appears.
# Thus, we are stuck with using accelerators instead of shortcuts.
# If Godot ever receives the ability to change the accelerator text of the items,
# we could in theory remove this class.
# If you don't care about PopupMenus in the same scene as ShortcutEdit
# such as projects like Pixelorama where everything is in the same scene,
# then you can ignore this class.
class MenuInputAction:
extends InputAction
var node_path := ""
var node: PopupMenu
var menu_item_id := 0
var echo := false
func _init(
_display_name := "",
_group := "",
_global := true,
_node_path := "",
_menu_item_id := 0,
_echo := false
) -> void:
._init(_display_name, _group, _global)
node_path = _node_path
menu_item_id = _menu_item_id
echo = _echo
func get_node(root: Node) -> void:
var temp_node = root.get_node(node_path)
if temp_node is PopupMenu:
node = node
elif temp_node is MenuButton:
node = temp_node.get_popup()
func update_node(action: String) -> void:
if !node:
return
var first_key: InputEventKey = Keychain.action_get_first_key(action)
var accel := first_key.get_scancode_with_modifiers() if first_key else 0
node.set_item_accelerator(menu_item_id, accel)
func handle_input(event: InputEvent, action: String) -> bool:
if not node:
return false
if event.is_action_pressed(action, false, true):
if event is InputEventKey:
var acc: int = node.get_item_accelerator(menu_item_id)
# If the event is the same as the menu item's accelerator, skip
if acc == event.get_scancode_with_modifiers():
return true
node.emit_signal("id_pressed", menu_item_id)
return true
if event.is_action(action, true) and echo:
if event.is_echo():
node.emit_signal("id_pressed", menu_item_id)
return true
return false
class InputGroup: class InputGroup:
var parent_group := "" var parent_group := ""
@ -112,21 +42,18 @@ class InputGroup:
func _ready() -> void: func _ready() -> void:
if !config_file: if !config_file:
config_file = ConfigFile.new() config_file = ConfigFile.new()
if !config_path.empty(): if !config_path.is_empty():
config_file.load(config_path) config_file.load(config_path)
set_process_input(multiple_menu_accelerators)
# Load shortcut profiles # Load shortcut profiles
var profile_dir := Directory.new() DirAccess.make_dir_recursive_absolute(PROFILES_PATH)
profile_dir.make_dir(PROFILES_PATH) var profile_dir := DirAccess.open(PROFILES_PATH)
profile_dir.open(PROFILES_PATH)
profile_dir.list_dir_begin() profile_dir.list_dir_begin()
var file_name = profile_dir.get_next() var file_name = profile_dir.get_next()
while file_name != "": while file_name != "":
if !profile_dir.current_is_dir(): if !profile_dir.current_is_dir():
if file_name.get_extension() == "tres": if file_name.get_extension() == "tres":
var file = load(PROFILES_PATH.plus_file(file_name)) var file = load(PROFILES_PATH.path_join(file_name))
if file is ShortcutProfile: if file is ShortcutProfile:
profiles.append(file) profiles.append(file)
file_name = profile_dir.get_next() file_name = profile_dir.get_next()
@ -135,7 +62,7 @@ func _ready() -> void:
if profiles.size() == 1: if profiles.size() == 1:
var profile := ShortcutProfile.new() var profile := ShortcutProfile.new()
profile.name = "Custom" profile.name = "Custom"
profile.resource_path = PROFILES_PATH.plus_file("custom.tres") profile.resource_path = PROFILES_PATH.path_join("custom.tres")
var saved := profile.save() var saved := profile.save()
if saved: if saved:
profiles.append(profile) profiles.append(profile)
@ -143,37 +70,9 @@ func _ready() -> void:
for profile in profiles: for profile in profiles:
profile.fill_bindings() profile.fill_bindings()
var l18n_dir := Directory.new()
l18n_dir.open(TRANSLATIONS_PATH)
l18n_dir.list_dir_begin()
file_name = l18n_dir.get_next()
while file_name != "":
if !l18n_dir.current_is_dir():
if file_name.get_extension() == "po":
var t: Translation = load(TRANSLATIONS_PATH.plus_file(file_name))
TranslationServer.add_translation(t)
file_name = l18n_dir.get_next()
profile_index = config_file.get_value("shortcuts", "shortcuts_profile", 0) profile_index = config_file.get_value("shortcuts", "shortcuts_profile", 0)
change_profile(profile_index) change_profile(profile_index)
for action in actions:
var input_action: InputAction = actions[action]
if input_action is MenuInputAction:
# Below line has been modified
input_action.get_node(Global.top_menu_container.get_node("MenuItems"))
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
return
for action in actions:
var input_action: InputAction = actions[action]
var done: bool = input_action.handle_input(event, action)
if done:
return
func change_profile(index: int) -> void: func change_profile(index: int) -> void:
if index >= profiles.size(): if index >= profiles.size():
@ -184,33 +83,21 @@ func change_profile(index: int) -> void:
action_erase_events(action) action_erase_events(action)
for event in selected_profile.bindings[action]: for event in selected_profile.bindings[action]:
action_add_event(action, event) action_add_event(action, event)
# NOTE: Following line not present in the plugin itself, be careful not to overwrite
Global.update_hint_tooltips()
func action_add_event(action: String, event: InputEvent) -> void: func action_add_event(action: String, event: InputEvent) -> void:
InputMap.action_add_event(action, event) InputMap.action_add_event(action, event)
if action in actions:
actions[action].update_node(action)
func action_erase_event(action: String, event: InputEvent) -> void: func action_erase_event(action: String, event: InputEvent) -> void:
InputMap.action_erase_event(action, event) InputMap.action_erase_event(action, event)
if action in actions:
actions[action].update_node(action)
func action_erase_events(action: String) -> void: func action_erase_events(action: String) -> void:
InputMap.action_erase_events(action) InputMap.action_erase_events(action)
if action in actions:
actions[action].update_node(action)
func action_get_first_key(action: String) -> InputEventKey: func load_translation(locale: String) -> void:
var first_key: InputEventKey = null var translation = load("res://addons/keychain/translations".path_join(locale + ".po"))
var events := InputMap.get_action_list(action) if is_instance_valid(translation) and translation is Translation:
for event in events: TranslationServer.add_translation(translation)
if event is InputEventKey:
first_key = event
break
return first_key

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Orama Interactive
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -2,7 +2,7 @@ extends Control
enum { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS } enum { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS }
const MOUSE_BUTTON_NAMES := [ const MOUSE_BUTTON_NAMES: PackedStringArray = [
"Left Button", "Left Button",
"Right Button", "Right Button",
"Middle Button", "Middle Button",
@ -14,7 +14,7 @@ const MOUSE_BUTTON_NAMES := [
"X Button 2", "X Button 2",
] ]
const JOY_BUTTON_NAMES := [ const JOY_BUTTON_NAMES: PackedStringArray = [
"DualShock Cross, Xbox A, Nintendo B", "DualShock Cross, Xbox A, Nintendo B",
"DualShock Circle, Xbox B, Nintendo A", "DualShock Circle, Xbox B, Nintendo A",
"DualShock Square, Xbox X, Nintendo Y", "DualShock Square, Xbox X, Nintendo Y",
@ -40,7 +40,7 @@ const JOY_BUTTON_NAMES := [
"PS4/5 Touchpad", "PS4/5 Touchpad",
] ]
const JOY_AXIS_NAMES := [ const JOY_AXIS_NAMES: PackedStringArray = [
"(Left Stick Left)", "(Left Stick Left)",
"(Left Stick Right)", "(Left Stick Right)",
"(Left Stick Up)", "(Left Stick Up)",
@ -66,29 +66,29 @@ const JOY_AXIS_NAMES := [
var currently_editing_tree_item: TreeItem var currently_editing_tree_item: TreeItem
var is_editing := false var is_editing := false
# Textures taken from Godot https://github.com/godotengine/godot/tree/master/editor/icons # Textures taken from Godot https://github.com/godotengine/godot/tree/master/editor/icons
var add_tex: Texture = preload("assets/add.svg") var add_tex: Texture2D = preload("assets/add.svg")
var edit_tex: Texture = preload("assets/edit.svg") var edit_tex: Texture2D = preload("assets/edit.svg")
var delete_tex: Texture = preload("assets/close.svg") var delete_tex: Texture2D = preload("assets/close.svg")
var joy_axis_tex: Texture = preload("assets/joy_axis.svg") var joy_axis_tex: Texture2D = preload("assets/joy_axis.svg")
var joy_button_tex: Texture = preload("assets/joy_button.svg") var joy_button_tex: Texture2D = preload("assets/joy_button.svg")
var key_tex: Texture = preload("assets/keyboard.svg") var key_tex: Texture2D = preload("assets/keyboard.svg")
var key_phys_tex: Texture = preload("assets/keyboard_physical.svg") var key_phys_tex: Texture2D = preload("assets/keyboard_physical.svg")
var mouse_tex: Texture = preload("assets/mouse.svg") var mouse_tex: Texture2D = preload("assets/mouse.svg")
var shortcut_tex: Texture = preload("assets/shortcut.svg") var shortcut_tex: Texture2D = preload("assets/shortcut.svg")
var folder_tex: Texture = preload("assets/folder.svg") var folder_tex: Texture2D = preload("assets/folder.svg")
onready var tree: Tree = $VBoxContainer/ShortcutTree @onready var tree: Tree = $VBoxContainer/ShortcutTree
onready var profile_option_button: OptionButton = find_node("ProfileOptionButton") @onready var profile_option_button: OptionButton = find_child("ProfileOptionButton")
onready var rename_profile_button: Button = find_node("RenameProfile") @onready var rename_profile_button: Button = find_child("RenameProfile")
onready var delete_profile_button: Button = find_node("DeleteProfile") @onready var delete_profile_button: Button = find_child("DeleteProfile")
onready var shortcut_type_menu: PopupMenu = $ShortcutTypeMenu @onready var shortcut_type_menu: PopupMenu = $ShortcutTypeMenu
onready var keyboard_shortcut_selector: ConfirmationDialog = $KeyboardShortcutSelectorDialog @onready var keyboard_shortcut_selector: ConfirmationDialog = $KeyboardShortcutSelectorDialog
onready var mouse_shortcut_selector: ConfirmationDialog = $MouseShortcutSelectorDialog @onready var mouse_shortcut_selector: ConfirmationDialog = $MouseShortcutSelectorDialog
onready var joy_key_shortcut_selector: ConfirmationDialog = $JoyKeyShortcutSelectorDialog @onready var joy_key_shortcut_selector: ConfirmationDialog = $JoyKeyShortcutSelectorDialog
onready var joy_axis_shortcut_selector: ConfirmationDialog = $JoyAxisShortcutSelectorDialog @onready var joy_axis_shortcut_selector: ConfirmationDialog = $JoyAxisShortcutSelectorDialog
onready var profile_settings: ConfirmationDialog = $ProfileSettings @onready var profile_settings: ConfirmationDialog = $ProfileSettings
onready var profile_name: LineEdit = $ProfileSettings/ProfileName @onready var profile_name: LineEdit = $ProfileSettings/ProfileName
onready var delete_confirmation: ConfirmationDialog = $DeleteConfirmation @onready var delete_confirmation: ConfirmationDialog = $DeleteConfirmation
func _ready() -> void: func _ready() -> void:
@ -107,7 +107,7 @@ func _ready() -> void:
profile_option_button.select(Keychain.profile_index) profile_option_button.select(Keychain.profile_index)
_on_ProfileOptionButton_item_selected(Keychain.profile_index) _on_ProfileOptionButton_item_selected(Keychain.profile_index)
if OS.get_name() == "HTML5": if OS.get_name() == "Web":
$VBoxContainer/HBoxContainer/OpenProfileFolder.queue_free() $VBoxContainer/HBoxContainer/OpenProfileFolder.queue_free()
@ -121,7 +121,7 @@ func _construct_tree() -> void:
for action in InputMap.get_actions(): # Fill the tree with actions and their events for action in InputMap.get_actions(): # Fill the tree with actions and their events
if action in Keychain.ignore_actions: if action in Keychain.ignore_actions:
continue continue
if Keychain.ignore_ui_actions and action.begins_with("ui_"): if Keychain.ignore_ui_actions and (action as String).begins_with("ui_"):
continue continue
var display_name := get_action_name(action) var display_name := get_action_name(action)
@ -131,7 +131,7 @@ func _construct_tree() -> void:
group_name = input_action.group group_name = input_action.group
var tree_item: TreeItem var tree_item: TreeItem
if group_name and group_name in Keychain.groups: if not group_name.is_empty() and group_name in Keychain.groups:
var input_group: Keychain.InputGroup = Keychain.groups[group_name] var input_group: Keychain.InputGroup = Keychain.groups[group_name]
var group_root: TreeItem = input_group.tree_item var group_root: TreeItem = input_group.tree_item
tree_item = tree.create_item(group_root) tree_item = tree.create_item(group_root)
@ -142,7 +142,7 @@ func _construct_tree() -> void:
tree_item.set_text(0, display_name) tree_item.set_text(0, display_name)
tree_item.set_metadata(0, action) tree_item.set_metadata(0, action)
tree_item.set_icon(0, shortcut_tex) tree_item.set_icon(0, shortcut_tex)
for event in InputMap.get_action_list(action): for event in InputMap.action_get_events(action):
add_event_tree_item(event, tree_item) add_event_tree_item(event, tree_item)
tree_item.add_button(0, add_tex, 0, buttons_disabled, "Add") tree_item.add_button(0, add_tex, 0, buttons_disabled, "Add")
@ -151,8 +151,6 @@ func _construct_tree() -> void:
func _fill_selector_options() -> void: func _fill_selector_options() -> void:
keyboard_shortcut_selector.entered_shortcut.visible = true
keyboard_shortcut_selector.option_button.visible = false
mouse_shortcut_selector.input_type_l.text = "Mouse Button Index:" mouse_shortcut_selector.input_type_l.text = "Mouse Button Index:"
joy_key_shortcut_selector.input_type_l.text = "Joypad Button Index:" joy_key_shortcut_selector.input_type_l.text = "Joypad Button Index:"
joy_axis_shortcut_selector.input_type_l.text = "Joypad Axis Index:" joy_axis_shortcut_selector.input_type_l.text = "Joypad Axis Index:"
@ -171,8 +169,8 @@ func _fill_selector_options() -> void:
var joy_axis_option_button: OptionButton = joy_axis_shortcut_selector.option_button var joy_axis_option_button: OptionButton = joy_axis_shortcut_selector.option_button
var i := 0.0 var i := 0.0
for option in JOY_AXIS_NAMES: for option in JOY_AXIS_NAMES:
var sign_symbol = "+" if floor(i) != i else "-" var sign_symbol := "+" if floori(i) != i else "-"
var text: String = tr("Axis") + " %s %s %s" % [floor(i), sign_symbol, tr(option)] var text: String = tr("Axis") + " %s %s %s" % [floori(i), sign_symbol, tr(option)]
joy_axis_option_button.add_item(text) joy_axis_option_button.add_item(text)
i += 0.5 i += 0.5
@ -200,7 +198,7 @@ func get_action_name(action: String) -> String:
if action in Keychain.actions: if action in Keychain.actions:
display_name = Keychain.actions[action].display_name display_name = Keychain.actions[action].display_name
if display_name.empty(): if display_name.is_empty():
display_name = _humanize_snake_case(action) display_name = _humanize_snake_case(action)
return display_name return display_name
@ -209,7 +207,7 @@ func _humanize_snake_case(text: String) -> String:
text = text.replace("_", " ") text = text.replace("_", " ")
var first_letter := text.left(1) var first_letter := text.left(1)
first_letter = first_letter.capitalize() first_letter = first_letter.capitalize()
text.erase(0, 1) text = text.right(-1)
text = text.insert(0, first_letter) text = text.insert(0, first_letter)
return text return text
@ -236,7 +234,7 @@ func add_event_tree_item(event: InputEvent, action_tree_item: TreeItem) -> void:
event_tree_item.set_metadata(0, event) event_tree_item.set_metadata(0, event)
match event_class: match event_class:
"InputEventKey": "InputEventKey":
var scancode: int = event.get_scancode_with_modifiers() var scancode: int = event.get_keycode_with_modifiers()
if scancode > 0: if scancode > 0:
event_tree_item.set_icon(0, key_tex) event_tree_item.set_icon(0, key_tex)
else: else:
@ -252,19 +250,10 @@ func add_event_tree_item(event: InputEvent, action_tree_item: TreeItem) -> void:
func event_to_str(event: InputEvent) -> String: func event_to_str(event: InputEvent) -> String:
var output := "" var output := event.as_text()
if event is InputEventKey: # event.as_text() could be used for these event types as well, but this gives more control
var scancode: int = event.get_scancode_with_modifiers() # to the developer as to what strings will be printed
var physical_str := "" if event is InputEventJoypadButton:
if scancode == 0:
scancode = event.get_physical_scancode_with_modifiers()
physical_str = " " + tr("(Physical)")
output = OS.get_scancode_string(scancode) + physical_str
elif event is InputEventMouseButton:
output = tr(MOUSE_BUTTON_NAMES[event.button_index - 1])
elif event is InputEventJoypadButton:
var button_index: int = event.button_index var button_index: int = event.button_index
output = tr("Button") output = tr("Button")
if button_index >= JOY_BUTTON_NAMES.size(): if button_index >= JOY_BUTTON_NAMES.size():
@ -281,24 +270,22 @@ func event_to_str(event: InputEvent) -> String:
return output return output
func _on_ShortcutTree_button_pressed(item: TreeItem, _column: int, id: int) -> void: func _on_shortcut_tree_button_clicked(item: TreeItem, _column: int, id: int, _mbi: int) -> void:
var action = item.get_metadata(0) var action = item.get_metadata(0)
currently_editing_tree_item = item currently_editing_tree_item = item
if action is String: if action is StringName:
if id == 0: # Add if id == 0: # Add
var rect: Rect2 = tree.get_item_area_rect(item, 0) var rect: Rect2 = tree.get_item_area_rect(item, 0)
rect.position.x = rect.end.x - 42 rect.position.x = rect.end.x - 42
rect.position.y += 42 - tree.get_scroll().y rect.position.y += 42 - tree.get_scroll().y
rect.position += rect_global_position rect.position += global_position
rect.size = Vector2(110, 23 * shortcut_type_menu.get_item_count()) rect.size = Vector2(110, 23 * shortcut_type_menu.get_item_count())
shortcut_type_menu.popup(rect) shortcut_type_menu.popup(rect)
elif id == 1: # Delete elif id == 1: # Delete
Keychain.action_erase_events(action) Keychain.action_erase_events(action)
Keychain.selected_profile.change_action(action) Keychain.selected_profile.change_action(action)
var child := item.get_children() for child in item.get_children():
while child != null:
child.free() child.free()
child = item.get_children()
elif action is InputEvent: elif action is InputEvent:
var parent_action = item.get_parent().get_metadata(0) var parent_action = item.get_parent().get_metadata(0)
@ -312,7 +299,7 @@ func _on_ShortcutTree_button_pressed(item: TreeItem, _column: int, id: int) -> v
elif action is InputEventJoypadMotion: elif action is InputEventJoypadMotion:
joy_axis_shortcut_selector.popup_centered() joy_axis_shortcut_selector.popup_centered()
elif id == 1: # Delete elif id == 1: # Delete
if not parent_action is String: if not parent_action is StringName:
return return
Keychain.action_erase_event(parent_action, action) Keychain.action_erase_event(parent_action, action)
Keychain.selected_profile.change_action(parent_action) Keychain.selected_profile.change_action(parent_action)
@ -322,7 +309,7 @@ func _on_ShortcutTree_button_pressed(item: TreeItem, _column: int, id: int) -> v
func _on_ShortcutTree_item_activated() -> void: func _on_ShortcutTree_item_activated() -> void:
var selected_item: TreeItem = tree.get_selected() var selected_item: TreeItem = tree.get_selected()
if selected_item.get_button_count(0) > 0 and !selected_item.is_button_disabled(0, 0): if selected_item.get_button_count(0) > 0 and !selected_item.is_button_disabled(0, 0):
_on_ShortcutTree_button_pressed(tree.get_selected(), 0, 0) _on_shortcut_tree_button_clicked(tree.get_selected(), 0, 0, 0)
func _on_ShortcutTypeMenu_id_pressed(id: int) -> void: func _on_ShortcutTypeMenu_id_pressed(id: int) -> void:
@ -353,14 +340,14 @@ func _on_ProfileOptionButton_item_selected(index: int) -> void:
func _on_NewProfile_pressed() -> void: func _on_NewProfile_pressed() -> void:
is_editing = false is_editing = false
profile_name.text = "New Shortcut Profile" profile_name.text = "New Shortcut Profile"
profile_settings.window_title = "New Shortcut Profile" profile_settings.title = "New Shortcut Profile"
profile_settings.popup_centered() profile_settings.popup_centered()
func _on_RenameProfile_pressed() -> void: func _on_RenameProfile_pressed() -> void:
is_editing = true is_editing = true
profile_name.text = Keychain.selected_profile.name profile_name.text = Keychain.selected_profile.name
profile_settings.window_title = "Rename Shortcut Profile" profile_settings.title = "Rename Shortcut Profile"
profile_settings.popup_centered() profile_settings.popup_centered()
@ -376,7 +363,7 @@ func _on_ProfileSettings_confirmed() -> void:
var file_name := profile_name.text + ".tres" var file_name := profile_name.text + ".tres"
var profile := ShortcutProfile.new() var profile := ShortcutProfile.new()
profile.name = profile_name.text profile.name = profile_name.text
profile.resource_path = Keychain.PROFILES_PATH.plus_file(file_name) profile.resource_path = Keychain.PROFILES_PATH.path_join(file_name)
profile.fill_bindings() profile.fill_bindings()
var saved := profile.save() var saved := profile.save()
if not saved: if not saved:
@ -397,14 +384,18 @@ func _on_ProfileSettings_confirmed() -> void:
func _delete_profile_file(file_name: String) -> void: func _delete_profile_file(file_name: String) -> void:
var dir := Directory.new() var dir := DirAccess.open(file_name.get_base_dir())
var err := dir.get_open_error()
if err != OK:
print("Error deleting shortcut profile %s. Error code: %s" % [file_name, err])
return
dir.remove(file_name) dir.remove(file_name)
func _on_DeleteConfirmation_confirmed() -> void: func _on_DeleteConfirmation_confirmed() -> void:
_delete_profile_file(Keychain.selected_profile.resource_path) _delete_profile_file(Keychain.selected_profile.resource_path)
profile_option_button.remove_item(Keychain.profile_index) profile_option_button.remove_item(Keychain.profile_index)
Keychain.profiles.remove(Keychain.profile_index) Keychain.profiles.remove_at(Keychain.profile_index)
Keychain.profile_index -= 1 Keychain.profile_index -= 1
if Keychain.profile_index < 0: if Keychain.profile_index < 0:
Keychain.profile_index = 0 Keychain.profile_index = 0

View file

@ -1,105 +1,108 @@
[gd_scene load_steps=7 format=2] [gd_scene load_steps=7 format=3 uid="uid://bq7ibhm0txl5p"]
[ext_resource path="res://addons/keychain/ShortcutEdit.gd" type="Script" id=1] [ext_resource type="Script" path="res://addons/keychain/ShortcutEdit.gd" id="1"]
[ext_resource path="res://addons/keychain/assets/joy_button.svg" type="Texture" id=2] [ext_resource type="Texture2D" uid="uid://ca58ufal2ufd8" path="res://addons/keychain/assets/joy_button.svg" id="2"]
[ext_resource path="res://addons/keychain/assets/keyboard.svg" type="Texture" id=3] [ext_resource type="Texture2D" uid="uid://c2s5rm4nec5yh" path="res://addons/keychain/assets/keyboard.svg" id="3"]
[ext_resource path="res://addons/keychain/assets/joy_axis.svg" type="Texture" id=4] [ext_resource type="Texture2D" uid="uid://bb6q6om3d08cm" path="res://addons/keychain/assets/joy_axis.svg" id="4"]
[ext_resource path="res://addons/keychain/assets/mouse.svg" type="Texture" id=5] [ext_resource type="Texture2D" uid="uid://bma7xj2rqqcr8" path="res://addons/keychain/assets/mouse.svg" id="5"]
[ext_resource path="res://addons/keychain/ShortcutSelectorDialog.tscn" type="PackedScene" id=6] [ext_resource type="PackedScene" uid="uid://bfjcafe2kvx7n" path="res://addons/keychain/ShortcutSelectorDialog.tscn" id="6"]
[node name="ShortcutEdit" type="VBoxContainer"] [node name="ShortcutEdit" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3 size_flags_vertical = 3
script = ExtResource( 1 ) script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
margin_right = 1280.0 layout_mode = 1
margin_bottom = 720.0 anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3 size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
margin_right = 1280.0 layout_mode = 2
margin_bottom = 20.0
[node name="ProfileLabel" type="Label" parent="VBoxContainer/HBoxContainer"] [node name="ProfileLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
margin_top = 3.0 layout_mode = 2
margin_right = 102.0
margin_bottom = 17.0
text = "Shortcut profile:" text = "Shortcut profile:"
[node name="ProfileOptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"] [node name="ProfileOptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"]
margin_left = 106.0 layout_mode = 2
margin_right = 135.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
[node name="NewProfile" type="Button" parent="VBoxContainer/HBoxContainer"] [node name="NewProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 139.0 layout_mode = 2
margin_right = 179.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
text = "New" text = "New"
[node name="RenameProfile" type="Button" parent="VBoxContainer/HBoxContainer"] [node name="RenameProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 183.0 layout_mode = 2
margin_right = 247.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
text = "Rename" text = "Rename"
[node name="DeleteProfile" type="Button" parent="VBoxContainer/HBoxContainer"] [node name="DeleteProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 251.0 layout_mode = 2
margin_right = 306.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
text = "Delete" text = "Delete"
[node name="OpenProfileFolder" type="Button" parent="VBoxContainer/HBoxContainer"] [node name="OpenProfileFolder" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 310.0 layout_mode = 2
margin_right = 401.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
text = "Open Folder" text = "Open Folder"
[node name="ShortcutTree" type="Tree" parent="VBoxContainer"] [node name="ShortcutTree" type="Tree" parent="VBoxContainer"]
margin_top = 24.0 layout_mode = 2
margin_right = 1280.0
margin_bottom = 720.0
size_flags_vertical = 3 size_flags_vertical = 3
hide_root = true hide_root = true
[node name="ShortcutTypeMenu" type="PopupMenu" parent="."] [node name="ShortcutTypeMenu" type="PopupMenu" parent="."]
margin_right = 20.0 size = Vector2i(154, 116)
margin_bottom = 20.0 item_count = 4
items = [ "Key", ExtResource( 3 ), 0, false, false, 0, 0, null, "", false, "Mouse Button", ExtResource( 5 ), 0, false, false, 1, 0, null, "", false, "Joy Button", ExtResource( 2 ), 0, false, false, 2, 0, null, "", false, "Joy Axis", ExtResource( 4 ), 0, false, false, 3, 0, null, "", false ] item_0/text = "Key"
item_0/icon = ExtResource("3")
item_0/id = 0
item_1/text = "Mouse Button"
item_1/icon = ExtResource("5")
item_1/id = 1
item_2/text = "Joy Button"
item_2/icon = ExtResource("2")
item_2/id = 2
item_3/text = "Joy Axis"
item_3/icon = ExtResource("4")
item_3/id = 3
[node name="KeyboardShortcutSelectorDialog" parent="." instance=ExtResource( 6 )] [node name="KeyboardShortcutSelectorDialog" parent="." instance=ExtResource("6")]
size = Vector2i(417, 134)
[node name="MouseShortcutSelectorDialog" parent="." instance=ExtResource( 6 )] [node name="MouseShortcutSelectorDialog" parent="." instance=ExtResource("6")]
size = Vector2i(417, 134)
input_type = 1 input_type = 1
[node name="JoyKeyShortcutSelectorDialog" parent="." instance=ExtResource( 6 )] [node name="JoyKeyShortcutSelectorDialog" parent="." instance=ExtResource("6")]
size = Vector2i(417, 134)
input_type = 2 input_type = 2
[node name="JoyAxisShortcutSelectorDialog" parent="." instance=ExtResource( 6 )] [node name="JoyAxisShortcutSelectorDialog" parent="." instance=ExtResource("6")]
size = Vector2i(417, 134)
input_type = 3 input_type = 3
[node name="ProfileSettings" type="ConfirmationDialog" parent="."] [node name="ProfileSettings" type="ConfirmationDialog" parent="."]
margin_right = 200.0
margin_bottom = 70.0
[node name="ProfileName" type="LineEdit" parent="ProfileSettings"] [node name="ProfileName" type="LineEdit" parent="ProfileSettings"]
margin_left = 8.0 offset_left = 8.0
margin_top = 8.0 offset_top = 8.0
margin_right = 192.0 offset_right = 192.0
margin_bottom = 34.0 offset_bottom = 51.0
caret_blink = true
caret_blink_speed = 0.5
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."] [node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
margin_right = 200.0 size = Vector2i(427, 100)
margin_bottom = 70.0
dialog_text = "Are you sure you want to delete this shortcut profile?" dialog_text = "Are you sure you want to delete this shortcut profile?"
[connection signal="item_selected" from="VBoxContainer/HBoxContainer/ProfileOptionButton" to="." method="_on_ProfileOptionButton_item_selected"] [connection signal="item_selected" from="VBoxContainer/HBoxContainer/ProfileOptionButton" to="." method="_on_ProfileOptionButton_item_selected"]
@ -107,7 +110,7 @@ dialog_text = "Are you sure you want to delete this shortcut profile?"
[connection signal="pressed" from="VBoxContainer/HBoxContainer/RenameProfile" to="." method="_on_RenameProfile_pressed"] [connection signal="pressed" from="VBoxContainer/HBoxContainer/RenameProfile" to="." method="_on_RenameProfile_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/DeleteProfile" to="." method="_on_DeleteProfile_pressed"] [connection signal="pressed" from="VBoxContainer/HBoxContainer/DeleteProfile" to="." method="_on_DeleteProfile_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/OpenProfileFolder" to="." method="_on_OpenProfileFolder_pressed"] [connection signal="pressed" from="VBoxContainer/HBoxContainer/OpenProfileFolder" to="." method="_on_OpenProfileFolder_pressed"]
[connection signal="button_pressed" from="VBoxContainer/ShortcutTree" to="." method="_on_ShortcutTree_button_pressed"] [connection signal="button_clicked" from="VBoxContainer/ShortcutTree" to="." method="_on_shortcut_tree_button_clicked"]
[connection signal="item_activated" from="VBoxContainer/ShortcutTree" to="." method="_on_ShortcutTree_item_activated"] [connection signal="item_activated" from="VBoxContainer/ShortcutTree" to="." method="_on_ShortcutTree_item_activated"]
[connection signal="id_pressed" from="ShortcutTypeMenu" to="." method="_on_ShortcutTypeMenu_id_pressed"] [connection signal="id_pressed" from="ShortcutTypeMenu" to="." method="_on_ShortcutTypeMenu_id_pressed"]
[connection signal="confirmed" from="ProfileSettings" to="." method="_on_ProfileSettings_confirmed"] [connection signal="confirmed" from="ProfileSettings" to="." method="_on_ProfileSettings_confirmed"]

View file

@ -1,9 +1,9 @@
class_name ShortcutProfile class_name ShortcutProfile
extends Resource extends Resource
export(String) var name := "" @export var name := ""
export(bool) var customizable := true @export var customizable := true
export(Dictionary) var bindings := {} @export var bindings := {}
func _init() -> void: func _init() -> void:
@ -14,7 +14,7 @@ func fill_bindings() -> void:
var unnecessary_actions = bindings.duplicate() # Checks if the profile has any unused actions var unnecessary_actions = bindings.duplicate() # Checks if the profile has any unused actions
for action in InputMap.get_actions(): for action in InputMap.get_actions():
if not action in bindings: if not action in bindings:
bindings[action] = InputMap.get_action_list(action) bindings[action] = InputMap.action_get_events(action)
unnecessary_actions.erase(action) unnecessary_actions.erase(action)
for action in unnecessary_actions: for action in unnecessary_actions:
bindings.erase(action) bindings.erase(action)
@ -24,14 +24,14 @@ func fill_bindings() -> void:
func change_action(action: String) -> void: func change_action(action: String) -> void:
if not customizable: if not customizable:
return return
bindings[action] = InputMap.get_action_list(action) bindings[action] = InputMap.action_get_events(action)
save() save()
func save() -> bool: func save() -> bool:
if !customizable: if !customizable:
return false return false
var err := ResourceSaver.save(resource_path, self) var err := ResourceSaver.save(self, resource_path)
if err != OK: if err != OK:
print("Error saving shortcut profile %s. Error code: %s" % [resource_path, err]) print("Error saving shortcut profile %s. Error code: %s" % [resource_path, err])
return false return false

View file

@ -2,28 +2,39 @@ extends ConfirmationDialog
enum InputTypes { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS } enum InputTypes { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS }
export(InputTypes) var input_type: int = InputTypes.KEYBOARD @export var input_type := InputTypes.KEYBOARD
var listened_input: InputEvent var listened_input: InputEvent
onready var root: Node = get_parent() @onready var root := get_parent()
onready var input_type_l: Label = $VBoxContainer/InputTypeLabel @onready var input_type_l := $VBoxContainer/InputTypeLabel as Label
onready var entered_shortcut: LineEdit = $VBoxContainer/EnteredShortcut @onready var entered_shortcut := $VBoxContainer/EnteredShortcut as LineEdit
onready var option_button: OptionButton = $VBoxContainer/OptionButton @onready var option_button := $VBoxContainer/OptionButton as OptionButton
onready var already_exists: Label = $VBoxContainer/AlreadyExistsLabel @onready var modifier_buttons := $VBoxContainer/ModifierButtons as HBoxContainer
@onready var alt_button := $VBoxContainer/ModifierButtons/Alt as CheckBox
@onready var shift_button := $VBoxContainer/ModifierButtons/Shift as CheckBox
@onready var control_button := $VBoxContainer/ModifierButtons/Control as CheckBox
@onready var meta_button := $VBoxContainer/ModifierButtons/Meta as CheckBox
@onready var command_control_button := $VBoxContainer/ModifierButtons/CommandOrControl as CheckBox
@onready var already_exists := $VBoxContainer/AlreadyExistsLabel as Label
func _ready() -> void: func _ready() -> void:
set_process_input(false) set_process_input(false)
if input_type == InputTypes.KEYBOARD: if input_type == InputTypes.KEYBOARD:
get_ok().focus_neighbour_top = entered_shortcut.get_path() entered_shortcut.visible = true
get_cancel().focus_neighbour_top = entered_shortcut.get_path() option_button.visible = false
entered_shortcut.focus_neighbour_bottom = get_ok().get_path() get_ok_button().focus_neighbor_top = entered_shortcut.get_path()
get_cancel_button().focus_neighbor_top = entered_shortcut.get_path()
entered_shortcut.focus_neighbor_bottom = get_ok_button().get_path()
else: else:
get_ok().focus_neighbour_top = option_button.get_path() if input_type != InputTypes.MOUSE:
get_cancel().focus_neighbour_top = option_button.get_path() modifier_buttons.visible = false
option_button.focus_neighbour_bottom = get_ok().get_path() get_ok_button().focus_neighbor_top = option_button.get_path()
get_cancel_button().focus_neighbor_top = option_button.get_path()
option_button.focus_neighbor_bottom = get_ok_button().get_path()
get_close_button().focus_mode = Control.FOCUS_NONE
# get_close_button().focus_mode = Control.FOCUS_NONE
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
@ -31,7 +42,8 @@ func _input(event: InputEvent) -> void:
return return
if event.pressed: if event.pressed:
listened_input = event listened_input = event
entered_shortcut.text = OS.get_scancode_string(event.get_scancode_with_modifiers()) _set_modifier_buttons_state(listened_input)
entered_shortcut.text = event.as_text()
_show_assigned_state(event) _show_assigned_state(event)
@ -40,7 +52,7 @@ func _show_assigned_state(event: InputEvent) -> void:
var action := "" var action := ""
if metadata is InputEvent: # Editing an input event if metadata is InputEvent: # Editing an input event
action = root.currently_editing_tree_item.get_parent().get_metadata(0) action = root.currently_editing_tree_item.get_parent().get_metadata(0)
elif metadata is String: # Adding a new input event to an action elif metadata is StringName: # Adding a new input event to an action
action = metadata action = metadata
var matching_pair: Array = _find_matching_event_in_map(action, event) var matching_pair: Array = _find_matching_event_in_map(action, event)
@ -65,14 +77,14 @@ func _apply_shortcut_change(input_event: InputEvent) -> void:
return return
root.currently_editing_tree_item.set_metadata(0, input_event) root.currently_editing_tree_item.set_metadata(0, input_event)
root.currently_editing_tree_item.set_text(0, root.event_to_str(input_event)) root.currently_editing_tree_item.set_text(0, root.event_to_str(input_event))
elif metadata is String: # Adding a new input event to an action elif metadata is StringName: # Adding a new input event to an action
var changed: bool = _set_shortcut(metadata, null, input_event) var changed: bool = _set_shortcut(metadata, null, input_event)
if !changed: if !changed:
return return
root.add_event_tree_item(input_event, root.currently_editing_tree_item) root.add_event_tree_item(input_event, root.currently_editing_tree_item)
func _set_shortcut(action: String, old_event: InputEvent, new_event: InputEvent) -> bool: func _set_shortcut(action: StringName, old_event: InputEvent, new_event: InputEvent) -> bool:
if InputMap.action_has_event(action, new_event): # If the current action already has that event if InputMap.action_has_event(action, new_event): # If the current action already has that event
return false return false
if old_event: if old_event:
@ -86,7 +98,7 @@ func _set_shortcut(action: String, old_event: InputEvent, new_event: InputEvent)
if action in Keychain.actions: if action in Keychain.actions:
group = Keychain.actions[action].group group = Keychain.actions[action].group
var action_to_replace: String = matching_pair[0] var action_to_replace: StringName = matching_pair[0]
var input_to_replace: InputEvent = matching_pair[1] var input_to_replace: InputEvent = matching_pair[1]
Keychain.action_erase_event(action_to_replace, input_to_replace) Keychain.action_erase_event(action_to_replace, input_to_replace)
Keychain.selected_profile.change_action(action_to_replace) Keychain.selected_profile.change_action(action_to_replace)
@ -95,34 +107,20 @@ func _set_shortcut(action: String, old_event: InputEvent, new_event: InputEvent)
while tree_item != null: # Loop through Tree's TreeItems... while tree_item != null: # Loop through Tree's TreeItems...
var metadata = tree_item.get_metadata(0) var metadata = tree_item.get_metadata(0)
if metadata is InputEvent: if metadata is InputEvent:
if input_to_replace.shortcut_match(metadata): if input_to_replace.is_match(metadata):
var map_action: String = tree_item.get_parent().get_metadata(0) var map_action: StringName = tree_item.get_parent().get_metadata(0)
if map_action == action_to_replace: if map_action == action_to_replace:
tree_item.free() tree_item.free()
break break
tree_item = _get_next_tree_item(tree_item) tree_item = tree_item.get_next_in_tree()
Keychain.action_add_event(action, new_event) Keychain.action_add_event(action, new_event)
Keychain.selected_profile.change_action(action) Keychain.selected_profile.change_action(action)
return true return true
# Based on https://github.com/godotengine/godot/blob/master/scene/gui/tree.cpp#L685 func _find_matching_event_in_map(action: StringName, event: InputEvent) -> Array:
func _get_next_tree_item(current: TreeItem) -> TreeItem:
if current.get_children():
current = current.get_children()
elif current.get_next():
current = current.get_next()
else:
while current and !current.get_next():
current = current.get_parent()
if current:
current = current.get_next()
return current
func _find_matching_event_in_map(action: String, event: InputEvent) -> Array:
var group := "" var group := ""
if action in Keychain.actions: if action in Keychain.actions:
group = Keychain.actions[action].group group = Keychain.actions[action].group
@ -130,10 +128,10 @@ func _find_matching_event_in_map(action: String, event: InputEvent) -> Array:
for map_action in InputMap.get_actions(): for map_action in InputMap.get_actions():
if map_action in Keychain.ignore_actions: if map_action in Keychain.ignore_actions:
continue continue
if Keychain.ignore_ui_actions and map_action.begins_with("ui_"): if Keychain.ignore_ui_actions and (map_action as String).begins_with("ui_"):
continue continue
for map_event in InputMap.get_action_list(map_action): for map_event in InputMap.action_get_events(map_action):
if !event.shortcut_match(map_event): if !event.is_match(map_event):
continue continue
if map_action in Keychain.actions: if map_action in Keychain.actions:
@ -148,27 +146,30 @@ func _find_matching_event_in_map(action: String, event: InputEvent) -> Array:
func _on_ShortcutSelectorDialog_about_to_show() -> void: func _on_ShortcutSelectorDialog_about_to_show() -> void:
var metadata = root.currently_editing_tree_item.get_metadata(0)
if input_type == InputTypes.KEYBOARD: if input_type == InputTypes.KEYBOARD:
listened_input = null listened_input = null
already_exists.text = "" already_exists.text = ""
entered_shortcut.text = "" entered_shortcut.text = ""
yield(get_tree(), "idle_frame") if metadata is InputEvent:
_set_modifier_buttons_state(metadata)
await get_tree().process_frame
entered_shortcut.grab_focus() entered_shortcut.grab_focus()
else: else:
var metadata = root.currently_editing_tree_item.get_metadata(0)
if metadata is InputEvent: # Editing an input event if metadata is InputEvent: # Editing an input event
var index := 0 var index := 0
if metadata is InputEventMouseButton: if metadata is InputEventMouseButton:
index = metadata.button_index - 1 index = metadata.button_index - 1
_set_modifier_buttons_state(metadata)
elif metadata is InputEventJoypadButton: elif metadata is InputEventJoypadButton:
index = metadata.button_index index = metadata.button_index
elif metadata is InputEventJoypadMotion: elif metadata is InputEventJoypadMotion:
index = metadata.axis * 2 index = metadata.axis * 2
index += round(metadata.axis_value) / 2.0 + 0.5 index += signi(metadata.axis_value) / 2.0 + 0.5
option_button.select(index) option_button.select(index)
_on_OptionButton_item_selected(index) _on_OptionButton_item_selected(index)
elif metadata is String: # Adding a new input event to an action elif metadata is StringName: # Adding a new input event to an action
option_button.select(0) option_button.select(0)
_on_OptionButton_item_selected(0) _on_OptionButton_item_selected(0)
@ -181,6 +182,11 @@ func _on_OptionButton_item_selected(index: int) -> void:
if input_type == InputTypes.MOUSE: if input_type == InputTypes.MOUSE:
listened_input = InputEventMouseButton.new() listened_input = InputEventMouseButton.new()
listened_input.button_index = index + 1 listened_input.button_index = index + 1
listened_input.alt_pressed = alt_button.button_pressed
listened_input.shift_pressed = shift_button.button_pressed
listened_input.ctrl_pressed = control_button.button_pressed
listened_input.meta_pressed = meta_button.button_pressed
listened_input.command_or_control_autoremap = command_control_button.button_pressed
elif input_type == InputTypes.JOY_BUTTON: elif input_type == InputTypes.JOY_BUTTON:
listened_input = InputEventJoypadButton.new() listened_input = InputEventJoypadButton.new()
listened_input.button_index = index listened_input.button_index = index
@ -197,3 +203,50 @@ func _on_EnteredShortcut_focus_entered() -> void:
func _on_EnteredShortcut_focus_exited() -> void: func _on_EnteredShortcut_focus_exited() -> void:
set_process_input(false) set_process_input(false)
func _on_alt_toggled(button_pressed: bool) -> void:
if not is_instance_valid(listened_input):
return
listened_input.alt_pressed = button_pressed
entered_shortcut.text = listened_input.as_text()
func _set_modifier_buttons_state(event: InputEventWithModifiers) -> void:
alt_button.button_pressed = event.alt_pressed
shift_button.button_pressed = event.shift_pressed
control_button.button_pressed = event.ctrl_pressed
meta_button.button_pressed = event.meta_pressed
command_control_button.button_pressed = event.command_or_control_autoremap
func _on_shift_toggled(button_pressed: bool) -> void:
if not is_instance_valid(listened_input):
return
listened_input.shift_pressed = button_pressed
entered_shortcut.text = listened_input.as_text()
func _on_control_toggled(button_pressed: bool) -> void:
if not is_instance_valid(listened_input):
return
listened_input.ctrl_pressed = button_pressed
entered_shortcut.text = listened_input.as_text()
func _on_meta_toggled(button_pressed: bool) -> void:
if not is_instance_valid(listened_input):
return
listened_input.meta_pressed = button_pressed
entered_shortcut.text = listened_input.as_text()
func _on_command_or_control_toggled(button_pressed: bool) -> void:
control_button.button_pressed = false
meta_button.button_pressed = false
control_button.visible = not button_pressed
meta_button.visible = not button_pressed
if not is_instance_valid(listened_input):
return
listened_input.command_or_control_autoremap = button_pressed
entered_shortcut.text = listened_input.as_text()

View file

@ -1,49 +1,69 @@
[gd_scene load_steps=2 format=2] [gd_scene load_steps=2 format=3 uid="uid://bfjcafe2kvx7n"]
[ext_resource path="res://addons/keychain/ShortcutSelectorDialog.gd" type="Script" id=1] [ext_resource type="Script" path="res://addons/keychain/ShortcutSelectorDialog.gd" id="1"]
[node name="ShortcutSelectorDialog" type="ConfirmationDialog"] [node name="ShortcutSelectorDialog" type="ConfirmationDialog"]
margin_right = 200.0 size = Vector2i(417, 169)
margin_bottom = 70.0 script = ExtResource("1")
popup_exclusive = true
window_title = "Set the shortcut"
script = ExtResource( 1 )
[node name="VBoxContainer" type="VBoxContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
margin_left = 8.0 offset_left = 8.0
margin_top = 8.0 offset_top = 8.0
margin_right = 341.0 offset_right = 409.0
margin_bottom = 64.0 offset_bottom = 120.0
[node name="InputTypeLabel" type="Label" parent="VBoxContainer"] [node name="InputTypeLabel" type="Label" parent="VBoxContainer"]
margin_right = 333.0 layout_mode = 2
margin_bottom = 14.0
text = "Press a key or a key combination to set the shortcut" text = "Press a key or a key combination to set the shortcut"
[node name="EnteredShortcut" type="LineEdit" parent="VBoxContainer"] [node name="EnteredShortcut" type="LineEdit" parent="VBoxContainer"]
visible = false visible = false
margin_top = 18.0 layout_mode = 2
margin_right = 333.0
margin_bottom = 32.0
align = 1
editable = false editable = false
virtual_keyboard_enabled = false virtual_keyboard_enabled = false
[node name="OptionButton" type="OptionButton" parent="VBoxContainer"] [node name="OptionButton" type="OptionButton" parent="VBoxContainer"]
margin_top = 18.0 layout_mode = 2
margin_right = 333.0
margin_bottom = 38.0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
[node name="AlreadyExistsLabel" type="Label" parent="VBoxContainer"] [node name="ModifierButtons" type="HBoxContainer" parent="VBoxContainer"]
margin_top = 42.0 layout_mode = 2
margin_right = 333.0
margin_bottom = 56.0
align = 1
[connection signal="about_to_show" from="." to="." method="_on_ShortcutSelectorDialog_about_to_show"] [node name="Alt" type="CheckBox" parent="VBoxContainer/ModifierButtons"]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Alt"
[node name="Shift" type="CheckBox" parent="VBoxContainer/ModifierButtons"]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Shift"
[node name="Control" type="CheckBox" parent="VBoxContainer/ModifierButtons"]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Control"
[node name="Meta" type="CheckBox" parent="VBoxContainer/ModifierButtons"]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Meta"
[node name="CommandOrControl" type="CheckBox" parent="VBoxContainer/ModifierButtons"]
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Command / Control (auto)"
[node name="AlreadyExistsLabel" type="Label" parent="VBoxContainer"]
layout_mode = 2
[connection signal="about_to_popup" from="." to="." method="_on_ShortcutSelectorDialog_about_to_show"]
[connection signal="confirmed" from="." to="." method="_on_ShortcutSelectorDialog_confirmed"] [connection signal="confirmed" from="." to="." method="_on_ShortcutSelectorDialog_confirmed"]
[connection signal="popup_hide" from="." to="." method="_on_ShortcutSelectorDialog_popup_hide"]
[connection signal="focus_entered" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_entered"] [connection signal="focus_entered" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_entered"]
[connection signal="focus_exited" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_exited"] [connection signal="focus_exited" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_exited"]
[connection signal="item_selected" from="VBoxContainer/OptionButton" to="." method="_on_OptionButton_item_selected"] [connection signal="item_selected" from="VBoxContainer/OptionButton" to="." method="_on_OptionButton_item_selected"]
[connection signal="toggled" from="VBoxContainer/ModifierButtons/Alt" to="." method="_on_alt_toggled"]
[connection signal="toggled" from="VBoxContainer/ModifierButtons/Shift" to="." method="_on_shift_toggled"]
[connection signal="toggled" from="VBoxContainer/ModifierButtons/Control" to="." method="_on_control_toggled"]
[connection signal="toggled" from="VBoxContainer/ModifierButtons/Meta" to="." method="_on_meta_toggled"]
[connection signal="toggled" from="VBoxContainer/ModifierButtons/CommandOrControl" to="." method="_on_command_or_control_toggled"]

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#e0e0e0" d="m7 1v6h-6v2h6v6h2v-6h6v-2h-6v-6z"/></svg> <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1v6h-6v2h6v6h2v-6h6v-2h-6v-6z" fill="#e0e0e0"/></svg>

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 149 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/add.svg-4084e314648c872072757f9b0f544cf9.stex" uid="uid://c4433pa25cxp2"
path="res://.godot/imported/add.svg-4084e314648c872072757f9b0f544cf9.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/add.svg" source_file="res://addons/keychain/assets/add.svg"
dest_files=[ "res://.import/add.svg-4084e314648c872072757f9b0f544cf9.stex" ] dest_files=["res://.godot/imported/add.svg-4084e314648c872072757f9b0f544cf9.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#e0e0e0" d="m3.7578 2.3438-1.4141 1.4141 4.2422 4.2422-4.2422 4.2422 1.4141 1.4141 4.2422-4.2422 4.2422 4.2422 1.4141-1.4141-4.2422-4.2422 4.2422-4.2422-1.4141-1.4141-4.2422 4.2422z"/></svg> <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m3.7578 2.3438-1.4141 1.4141 4.2422 4.2422-4.2422 4.2422 1.4141 1.4141 4.2422-4.2422 4.2422 4.2422 1.4141-1.4141-4.2422-4.2422 4.2422-4.2422-1.4141-1.4141-4.2422 4.2422z" fill="#e0e0e0"/></svg>

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 286 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.stex" uid="uid://f1gcylay6c6f"
path="res://.godot/imported/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/close.svg" source_file="res://addons/keychain/assets/close.svg"
dest_files=[ "res://.import/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.stex" ] dest_files=["res://.godot/imported/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#e0e0e0" d="m7 1c-.554 0-1 .446-1 1v2h4v-2c0-.554-.446-1-1-1zm-1 4v7l2 3 2-3v-7zm1 1h1v5h-1z"/></svg> <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1c-.554 0-1 .446-1 1v2h4v-2c0-.554-.446-1-1-1zm-1 4v7l2 3 2-3v-7zm1 1h1v5h-1z" fill="#e0e0e0"/></svg>

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 197 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/edit.svg-cd9834545a8696f1e8611efa12a48f33.stex" uid="uid://df8gl3u2nqpl3"
path="res://.godot/imported/edit.svg-cd9834545a8696f1e8611efa12a48f33.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/edit.svg" source_file="res://addons/keychain/assets/edit.svg"
dest_files=[ "res://.import/edit.svg-cd9834545a8696f1e8611efa12a48f33.stex" ] dest_files=["res://.godot/imported/edit.svg-cd9834545a8696f1e8611efa12a48f33.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#e0e0e0" d="m2 2a1 1 0 0 0 -1 1v2 6 2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-7a1 1 0 0 0 -1-1h-4a1 1 0 0 1 -1-1v-1a1 1 0 0 0 -1-1z"/></svg> <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m2 2a1 1 0 0 0 -1 1v2 6 2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-7a1 1 0 0 0 -1-1h-4a1 1 0 0 1 -1-1v-1a1 1 0 0 0 -1-1z" fill="#e0e0e0"/></svg>

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 228 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/folder.svg-490bb7e2d2aa4425de998b1d382cf534.stex" uid="uid://b0gbmkb8xwksb"
path="res://.godot/imported/folder.svg-490bb7e2d2aa4425de998b1d382cf534.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/folder.svg" source_file="res://addons/keychain/assets/folder.svg"
dest_files=[ "res://.import/folder.svg-490bb7e2d2aa4425de998b1d382cf534.stex" ] dest_files=["res://.godot/imported/folder.svg-490bb7e2d2aa4425de998b1d382cf534.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.stex" uid="uid://bb6q6om3d08cm"
path="res://.godot/imported/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/joy_axis.svg" source_file="res://addons/keychain/assets/joy_axis.svg"
dest_files=[ "res://.import/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.stex" ] dest_files=["res://.godot/imported/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/joy_button.svg-df5663c6f296cab556e81b9c770699d0.stex" uid="uid://ca58ufal2ufd8"
path="res://.godot/imported/joy_button.svg-df5663c6f296cab556e81b9c770699d0.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/joy_button.svg" source_file="res://addons/keychain/assets/joy_button.svg"
dest_files=[ "res://.import/joy_button.svg-df5663c6f296cab556e81b9c770699d0.stex" ] dest_files=["res://.godot/imported/joy_button.svg-df5663c6f296cab556e81b9c770699d0.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill-opacity=".996"><path fill="#e0e0e0" d="m4 2a1 1 0 0 0 -1 1v9.084c0 .506.448.916 1 .916h8c.552 0 1-.41 1-.916v-9.084a1 1 0 0 0 -1-1zm1.543 1.139h1.393l1.834 4.199h1.295v.437c.708.052 1.246.239 1.61.559.368.316.55.747.55 1.295 0 .552-.182.99-.55 1.314-.368.32-.906.505-1.61.553v.467h-1.294v-.473c-.708-.06-1.247-.248-1.615-.564-.364-.316-.545-.75-.545-1.297 0-.548.181-.977.545-1.29.368-.315.907-.504 1.615-.564v-.437h-1.464l-.282-.733h-1.595l-.284.733h-1.439l1.836-4.2zm.684 1.39-.409 1.057h.817zm3.84 4.338v1.526c.28-.04.483-.12.607-.24.124-.125.185-.302.185-.53 0-.224-.063-.396-.191-.516-.124-.12-.326-.2-.602-.24zm-1.296.006c-.284.04-.487.12-.61.24-.12.116-.182.288-.182.516 0 .22.065.392.193.512.132.12.331.202.6.246v-1.514z"/><path fill="#fff" d="m27 2h7v14h-7z"/><path fill="#e0e0e0" d="m1 4v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9z"/></g></svg> <svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill-opacity=".996"><path d="m4 2a1 1 0 0 0 -1 1v9.084c0 .506.448.916 1 .916h8c.552 0 1-.41 1-.916v-9.084a1 1 0 0 0 -1-1zm1.543 1.139h1.393l1.834 4.199h1.295v.437c.708.052 1.246.239 1.61.559.368.316.55.747.55 1.295 0 .552-.182.99-.55 1.314-.368.32-.906.505-1.61.553v.467h-1.294v-.473c-.708-.06-1.247-.248-1.615-.564-.364-.316-.545-.75-.545-1.297 0-.548.181-.977.545-1.29.368-.315.907-.504 1.615-.564v-.437h-1.464l-.282-.733h-1.595l-.284.733h-1.439l1.836-4.2zm.684 1.39-.409 1.057h.817zm3.84 4.338v1.526c.28-.04.483-.12.607-.24.124-.125.185-.302.185-.53 0-.224-.063-.396-.191-.516-.124-.12-.326-.2-.602-.24zm-1.296.006c-.284.04-.487.12-.61.24-.12.116-.182.288-.182.516 0 .22.065.392.193.512.132.12.331.202.6.246v-1.514z" fill="#e0e0e0"/><path d="m27 2h7v14h-7z" fill="#fff"/><path d="m1 4v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9z" fill="#e0e0e0"/></g></svg>

Before

Width:  |  Height:  |  Size: 960 B

After

Width:  |  Height:  |  Size: 961 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.stex" uid="uid://c2s5rm4nec5yh"
path="res://.godot/imported/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/keyboard.svg" source_file="res://addons/keychain/assets/keyboard.svg"
dest_files=[ "res://.import/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.stex" ] dest_files=["res://.godot/imported/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill-opacity=".996"><path fill="#e0e0e0" d="m4 2a1 1 0 0 0 -1 1v9.084c0 .506.448.916 1 .916h8c.552 0 1-.41 1-.916v-9.084a1 1 0 0 0 -1-1zm2.762 1.768h2.476l3.264 7.464h-2.604l-.502-1.3h-2.835l-.502 1.3h-2.561zm1.217 2.474-.725 1.878h1.45z"/><path fill="#fff" d="m27 2h7v14h-7z"/><path fill="#e0e0e0" d="m1 4v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9z"/></g></svg> <svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill-opacity=".996"><path d="m4 2a1 1 0 0 0 -1 1v9.084c0 .506.448.916 1 .916h8c.552 0 1-.41 1-.916v-9.084a1 1 0 0 0 -1-1zm2.762 1.768h2.476l3.264 7.464h-2.604l-.502-1.3h-2.835l-.502 1.3h-2.561zm1.217 2.474-.725 1.878h1.45z" fill="#e0e0e0"/><path d="m27 2h7v14h-7z" fill="#fff"/><path d="m1 4v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9z" fill="#e0e0e0"/></g></svg>

Before

Width:  |  Height:  |  Size: 464 B

After

Width:  |  Height:  |  Size: 465 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.stex" uid="uid://cmh8eaibhn5y8"
path="res://.godot/imported/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/keyboard_physical.svg" source_file="res://addons/keychain/assets/keyboard_physical.svg"
dest_files=[ "res://.import/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.stex" ] dest_files=["res://.godot/imported/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#e0e0e0" d="m7 1.1016a5 5 0 0 0 -4 4.8984h4zm2 .0039063v4.8945h4a5 5 0 0 0 -4-4.8945zm-6 6.8945v2a5 5 0 0 0 5 5 5 5 0 0 0 5-5v-2z"/></svg> <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1.1016a5 5 0 0 0 -4 4.8984h4zm2 .0039063v4.8945h4a5 5 0 0 0 -4-4.8945zm-6 6.8945v2a5 5 0 0 0 5 5 5 5 0 0 0 5-5v-2z" fill="#e0e0e0"/></svg>

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 234 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.stex" uid="uid://bma7xj2rqqcr8"
path="res://.godot/imported/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/mouse.svg" source_file="res://addons/keychain/assets/mouse.svg"
dest_files=[ "res://.import/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.stex" ] dest_files=["res://.godot/imported/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#e0e0e0" fill-opacity=".996" d="m4 2c-.55228 0-1 .4477-1 1v9.084c.0004015.506.448.91602 1 .91602h8c.552 0 .9996-.41002 1-.91602v-9.084c0-.5523-.44772-1-1-1zm-3 2v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a.99998.99998 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9zm6 0h3l-1 3h2l-4 4 1-3h-2z"/></svg> <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m4 2c-.55228 0-1 .4477-1 1v9.084c.0004015.506.448.91602 1 .91602h8c.552 0 .9996-.41002 1-.91602v-9.084c0-.5523-.44772-1-1-1zm-3 2v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a.99998.99998 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9zm6 0h3l-1 3h2l-4 4 1-3h-2z" fill="#e0e0e0" fill-opacity=".99608"/></svg>

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 382 B

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/shortcut.svg-401daac18a817142e2b29e5801e00053.stex" uid="uid://bmi24jp4fqi7k"
path="res://.godot/imported/shortcut.svg-401daac18a817142e2b29e5801e00053.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://addons/keychain/assets/shortcut.svg" source_file="res://addons/keychain/assets/shortcut.svg"
dest_files=[ "res://.import/shortcut.svg-401daac18a817142e2b29e5801e00053.stex" ] dest_files=["res://.godot/imported/shortcut.svg-401daac18a817142e2b29e5801e00053.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=true mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -3,5 +3,5 @@
name="Keychain" name="Keychain"
description="A plugin for the Godot Engine that aims to give the player full control over the input actions of the game." description="A plugin for the Godot Engine that aims to give the player full control over the input actions of the game."
author="Orama Interactive" author="Orama Interactive"
version="1.1" version="2.0"
script="plugin.gd" script="plugin.gd"

View file

@ -1,4 +1,4 @@
tool @tool
extends EditorPlugin extends EditorPlugin

View file

@ -1,10 +1,9 @@
[gd_resource type="Resource" load_steps=2 format=2] [gd_resource type="Resource" script_class="ShortcutProfile" load_steps=2 format=3 uid="uid://df04uev1epmmo"]
[ext_resource path="res://addons/keychain/ShortcutProfile.gd" type="Script" id=1] [ext_resource type="Script" path="res://addons/keychain/ShortcutProfile.gd" id="1"]
[resource] [resource]
script = ExtResource( 1 ) script = ExtResource("1")
name = "Default" name = "Default"
customizable = false customizable = false
bindings = { bindings = {}
}

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/bayer16.png-48f80e5e8f0bdac9e83b3acfa8588cf2.stex" uid="uid://cdtpmo4pjjjdd"
path="res://.godot/imported/bayer16.png-48f80e5e8f0bdac9e83b3acfa8588cf2.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/dither-matrices/bayer16.png" source_file="res://assets/dither-matrices/bayer16.png"
dest_files=[ "res://.import/bayer16.png-48f80e5e8f0bdac9e83b3acfa8588cf2.stex" ] dest_files=["res://.godot/imported/bayer16.png-48f80e5e8f0bdac9e83b3acfa8588cf2.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=1 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/bayer2.png-c060ec2442edc204b9ec0ab2d99e19bf.stex" uid="uid://cdoxrolk7qdju"
path="res://.godot/imported/bayer2.png-c060ec2442edc204b9ec0ab2d99e19bf.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/dither-matrices/bayer2.png" source_file="res://assets/dither-matrices/bayer2.png"
dest_files=[ "res://.import/bayer2.png-c060ec2442edc204b9ec0ab2d99e19bf.stex" ] dest_files=["res://.godot/imported/bayer2.png-c060ec2442edc204b9ec0ab2d99e19bf.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=1 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/bayer4.png-2008f73a4dd6b2f9f4ba6aac825a7b9b.stex" uid="uid://oald6mp1l1h7"
path="res://.godot/imported/bayer4.png-2008f73a4dd6b2f9f4ba6aac825a7b9b.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/dither-matrices/bayer4.png" source_file="res://assets/dither-matrices/bayer4.png"
dest_files=[ "res://.import/bayer4.png-2008f73a4dd6b2f9f4ba6aac825a7b9b.stex" ] dest_files=["res://.godot/imported/bayer4.png-2008f73a4dd6b2f9f4ba6aac825a7b9b.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=1 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/bayer8.png-2c0f9ff189b3d219f822ce84a94f5abd.stex" uid="uid://bq45j7jxav8nw"
path="res://.godot/imported/bayer8.png-2c0f9ff189b3d219f822ce84a94f5abd.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/dither-matrices/bayer8.png" source_file="res://assets/dither-matrices/bayer8.png"
dest_files=[ "res://.import/bayer8.png-2c0f9ff189b3d219f822ce84a94f5abd.stex" ] dest_files=["res://.godot/imported/bayer8.png-2c0f9ff189b3d219f822ce84a94f5abd.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=1 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,18 +0,0 @@
Copyright (C) 2008 The Android Open Source Project
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.
##########
This directory contains the fonts for the platform. They are licensed
under the Apache 2 license.

Binary file not shown.

View file

@ -1,10 +0,0 @@
[gd_resource type="DynamicFont" load_steps=3 format=2]
[ext_resource path="res://assets/fonts/Roboto-Regular.ttf" type="DynamicFontData" id=1]
[ext_resource path="res://assets/fonts/DroidSansFallback.ttf" type="DynamicFontData" id=2]
[resource]
use_mipmaps = true
use_filter = true
font_data = ExtResource( 1 )
fallback/0 = ExtResource( 2 )

View file

@ -1,11 +0,0 @@
[gd_resource type="DynamicFont" load_steps=3 format=2]
[ext_resource path="res://assets/fonts/Roboto-Italic.ttf" type="DynamicFontData" id=1]
[ext_resource path="res://assets/fonts/DroidSansFallback.ttf" type="DynamicFontData" id=2]
[resource]
size = 12
use_mipmaps = true
use_filter = true
font_data = ExtResource( 1 )
fallback/0 = ExtResource( 2 )

View file

@ -0,0 +1,33 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cy5lkye8vdr5t"
path="res://.godot/imported/Roboto-Italic.ttf-1459ab7510a4240cd642ef0fe9caff7f.fontdata"
[deps]
source_file="res://assets/fonts/Roboto-Italic.ttf"
dest_files=["res://.godot/imported/Roboto-Italic.ttf-1459ab7510a4240cd642ef0fe9caff7f.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View file

@ -1,11 +0,0 @@
[gd_resource type="DynamicFont" load_steps=3 format=2]
[ext_resource path="res://assets/fonts/Roboto-Regular.ttf" type="DynamicFontData" id=1]
[ext_resource path="res://assets/fonts/DroidSansFallback.ttf" type="DynamicFontData" id=2]
[resource]
size = 12
use_mipmaps = true
use_filter = true
font_data = ExtResource( 1 )
fallback/0 = ExtResource( 2 )

View file

@ -0,0 +1,33 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cvc4lelf6hl7x"
path="res://.godot/imported/Roboto-Regular.ttf-e67097a08cc051e6b179dbaab401b2bc.fontdata"
[deps]
source_file="res://assets/fonts/Roboto-Regular.ttf"
dest_files=["res://.godot/imported/Roboto-Regular.ttf-e67097a08cc051e6b179dbaab401b2bc.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View file

@ -1,10 +0,0 @@
[gd_resource type="DynamicFont" load_steps=3 format=2]
[ext_resource path="res://assets/fonts/Roboto-Regular.ttf" type="DynamicFontData" id=1]
[ext_resource path="res://assets/fonts/DroidSansFallback.ttf" type="DynamicFontData" id=2]
[resource]
size = 10
use_mipmaps = true
font_data = ExtResource( 1 )
fallback/0 = ExtResource( 2 )

View file

@ -2,12 +2,13 @@
importer="image" importer="image"
type="Image" type="Image"
path="res://.import/circle_9x9.png-775114b180b1f2b8d06dc42243fc410b.image" uid="uid://bccayivs7v851"
path="res://.godot/imported/circle_9x9.png-775114b180b1f2b8d06dc42243fc410b.image"
[deps] [deps]
source_file="res://assets/graphics/circle_9x9.png" source_file="res://assets/graphics/circle_9x9.png"
dest_files=[ "res://.import/circle_9x9.png-775114b180b1f2b8d06dc42243fc410b.image" ] dest_files=["res://.godot/imported/circle_9x9.png-775114b180b1f2b8d06dc42243fc410b.image"]
[params] [params]

View file

@ -2,12 +2,13 @@
importer="image" importer="image"
type="Image" type="Image"
path="res://.import/circle_filled_9x9.png-babef9d7f189abd07273845268222c6c.image" uid="uid://0rkdth7xh8ah"
path="res://.godot/imported/circle_filled_9x9.png-babef9d7f189abd07273845268222c6c.image"
[deps] [deps]
source_file="res://assets/graphics/circle_filled_9x9.png" source_file="res://assets/graphics/circle_filled_9x9.png"
dest_files=[ "res://.import/circle_filled_9x9.png-babef9d7f189abd07273845268222c6c.image" ] dest_files=["res://.godot/imported/circle_filled_9x9.png-babef9d7f189abd07273845268222c6c.image"]
[params] [params]

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/cursor.png-45ecd5a1e583fe3ca68fe559623aadce.stex" uid="uid://busi7lojhqxgq"
path="res://.godot/imported/cursor.png-45ecd5a1e583fe3ca68fe559623aadce.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/cursor.png" source_file="res://assets/graphics/cursor.png"
dest_files=[ "res://.import/cursor.png-45ecd5a1e583fe3ca68fe559623aadce.stex" ] dest_files=["res://.godot/imported/cursor.png-45ecd5a1e583fe3ca68fe559623aadce.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/dotted_line.png-9a94e4e6ebf9c11e53076f53e529d37a.stex" uid="uid://xnqvppcpt7jb"
path="res://.godot/imported/dotted_line.png-9a94e4e6ebf9c11e53076f53e529d37a.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/dotted_line.png" source_file="res://assets/graphics/dotted_line.png"
dest_files=[ "res://.import/dotted_line.png-9a94e4e6ebf9c11e53076f53e529d37a.stex" ] dest_files=["res://.godot/imported/dotted_line.png-9a94e4e6ebf9c11e53076f53e529d37a.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=1 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/directional_light.svg-093cdb9a72dee590271da014181a4680.stex" uid="uid://bd25ysikhyhqn"
path="res://.godot/imported/directional_light.svg-093cdb9a72dee590271da014181a4680.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/gizmos/directional_light.svg" source_file="res://assets/graphics/gizmos/directional_light.svg"
dest_files=[ "res://.import/directional_light.svg-093cdb9a72dee590271da014181a4680.stex" ] dest_files=["res://.godot/imported/directional_light.svg-093cdb9a72dee590271da014181a4680.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.stex" uid="uid://cu38hb880ltae"
path="res://.godot/imported/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/gizmos/omni_light.svg" source_file="res://assets/graphics/gizmos/omni_light.svg"
dest_files=[ "res://.import/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.stex" ] dest_files=["res://.godot/imported/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.stex" uid="uid://ded88bscdjle8"
path="res://.godot/imported/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,27 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/gizmos/spot_light.svg" source_file="res://assets/graphics/gizmos/spot_light.svg"
dest_files=[ "res://.import/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.stex" ] dest_files=["res://.godot/imported/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/icon.png-aa34c86f977c2e615fbfe8e823c3b00c.stex" uid="uid://b47r0c6auaqk6"
path="res://.godot/imported/icon.png-aa34c86f977c2e615fbfe8e823c3b00c.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/icons/icon.png" source_file="res://assets/graphics/icons/icon.png"
dest_files=[ "res://.import/icon.png-aa34c86f977c2e615fbfe8e823c3b00c.stex" ] dest_files=["res://.godot/imported/icon.png-aa34c86f977c2e615fbfe8e823c3b00c.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/clone.png-04ad708db9ea2b00606e50fe99f4ca29.stex" uid="uid://d3gx4phcox58s"
path="res://.godot/imported/clone.png-04ad708db9ea2b00606e50fe99f4ca29.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/layers/clone.png" source_file="res://assets/graphics/layers/clone.png"
dest_files=[ "res://.import/clone.png-04ad708db9ea2b00606e50fe99f4ca29.stex" ] dest_files=["res://.godot/imported/clone.png-04ad708db9ea2b00606e50fe99f4ca29.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/delete.png-78cc7779425d13dd019b2b6b51aca5b2.stex" uid="uid://x2k652y15v04"
path="res://.godot/imported/delete.png-78cc7779425d13dd019b2b6b51aca5b2.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/layers/delete.png" source_file="res://assets/graphics/layers/delete.png"
dest_files=[ "res://.import/delete.png-78cc7779425d13dd019b2b6b51aca5b2.stex" ] dest_files=["res://.godot/imported/delete.png-78cc7779425d13dd019b2b6b51aca5b2.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/group_collapsed.png-9d08fac1c2f635c754860111d024aa0f.stex" uid="uid://d11kh3e3nq1l5"
path="res://.godot/imported/group_collapsed.png-9d08fac1c2f635c754860111d024aa0f.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/layers/group_collapsed.png" source_file="res://assets/graphics/layers/group_collapsed.png"
dest_files=[ "res://.import/group_collapsed.png-9d08fac1c2f635c754860111d024aa0f.stex" ] dest_files=["res://.godot/imported/group_collapsed.png-9d08fac1c2f635c754860111d024aa0f.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/group_expanded.png-f3cd620185a4989737d6d9a36f4b57f3.stex" uid="uid://dndlglvqc7v6a"
path="res://.godot/imported/group_expanded.png-f3cd620185a4989737d6d9a36f4b57f3.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/layers/group_expanded.png" source_file="res://assets/graphics/layers/group_expanded.png"
dest_files=[ "res://.import/group_expanded.png-f3cd620185a4989737d6d9a36f4b57f3.stex" ] dest_files=["res://.godot/imported/group_expanded.png-f3cd620185a4989737d6d9a36f4b57f3.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/group_new.png-4ebdc7dd84d8c8a7b7979f50f4471543.stex" uid="uid://cmgnc2emsbxni"
path="res://.godot/imported/group_new.png-4ebdc7dd84d8c8a7b7979f50f4471543.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/layers/group_new.png" source_file="res://assets/graphics/layers/group_new.png"
dest_files=[ "res://.import/group_new.png-4ebdc7dd84d8c8a7b7979f50f4471543.stex" ] dest_files=["res://.godot/imported/group_new.png-4ebdc7dd84d8c8a7b7979f50f4471543.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=false process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/layer_invisible.png-b8a35d26d7a70c1d9e228e24fcffb0d2.stex" uid="uid://dwqdm6pllnipj"
path="res://.godot/imported/layer_invisible.png-b8a35d26d7a70c1d9e228e24fcffb0d2.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/layers/layer_invisible.png" source_file="res://assets/graphics/layers/layer_invisible.png"
dest_files=[ "res://.import/layer_invisible.png-b8a35d26d7a70c1d9e228e24fcffb0d2.stex" ] dest_files=["res://.godot/imported/layer_invisible.png-b8a35d26d7a70c1d9e228e24fcffb0d2.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap] [remap]
importer="texture" importer="texture"
type="StreamTexture" type="CompressedTexture2D"
path="res://.import/layer_visible.png-28b5ac4566a2e156d8e7d4caecc5f889.stex" uid="uid://c2b3htff5yox8"
path="res://.godot/imported/layer_visible.png-28b5ac4566a2e156d8e7d4caecc5f889.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -10,26 +11,24 @@ metadata={
[deps] [deps]
source_file="res://assets/graphics/layers/layer_visible.png" source_file="res://assets/graphics/layers/layer_visible.png"
dest_files=[ "res://.import/layer_visible.png-28b5ac4566a2e156d8e7d4caecc5f889.stex" ] dest_files=["res://.godot/imported/layer_visible.png-28b5ac4566a2e156d8e7d4caecc5f889.ctex"]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_mode=0 compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
flags/repeat=0 compress/channel_pack=0
flags/filter=false mipmaps/generate=false
flags/mipmaps=false mipmaps/limit=-1
flags/anisotropic=false roughness/mode=0
flags/srgb=2 roughness/src_normal=""
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
stream=false process/hdr_as_srgb=false
size_limit=0 process/hdr_clamp_exposure=false
detect_3d=true process/size_limit=0
svg/scale=1.0 detect_3d/compress_to=1

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