1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 09:09:47 +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:
- no-elif-return
- no-else-return
- max-returns
- private-method-call
max-file-lines: 2000

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install gdtoolkit
run: pip3 install "gdtoolkit==3.*"
run: pip3 install "gdtoolkit==4.*"
- name: Formatting checks
run: gdformat --diff .
- 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.
<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
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))

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).
## 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.
## Current features:

View file

@ -1655,27 +1655,21 @@ msgid "Current frame as spritesheet"
msgstr ""
msgid "Jump to the first frame\n"
"(%s)"
msgstr ""
msgid "Go to the previous frame\n"
"(%s)"
msgstr ""
msgid "Play the animation backwards (from end to beginning)\n"
"(%s)"
msgstr ""
msgid "Play the animation forward (from beginning to end)\n"
"(%s)"
msgstr ""
msgid "Go to the next frame\n"
"(%s)"
msgstr ""
msgid "Jump to the last frame\n"
"(%s)"
msgstr ""
msgid "Onion Skinning settings"
@ -2614,6 +2608,10 @@ msgstr ""
msgid "Backing out at ends"
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"
msgstr ""

View file

@ -3,19 +3,19 @@
## aimgio
- Upstream: https://gitlab.com/20kdc/20kdc_godot_addons
- Version: Presently git commit b6a1411758c856ad543f4f661ca4105aa7c0ab6d
- License: [Unlicense](https://gitlab.com/20kdc/20kdc_godot_addons/-/blob/b6a1411758c856ad543f4f661ca4105aa7c0ab6d/addons/aimg_io/COPYING.txt)
- Version: ba00188e9da1ae229181f106788fcb72ccdd85fa
- License: [Unlicense](https://gitlab.com/20kdc/20kdc_godot_addons/-/blob/master/godot4/addons/aimg_io/COPYING.txt)
## 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)
## gdgifexporter
- Upstream: https://github.com/jegor377/godot-gdgifexporter
- Version: custom
- Version: c368952a97bbbbcfa491358a61043be7009536a6
- License: [MIT](https://github.com/jegor377/godot-gdgifexporter/blob/master/LICENSE)
Files extracted from source:
@ -24,13 +24,13 @@ Files extracted from source:
## 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)
## 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)

View file

@ -1,19 +1,42 @@
class_name RegionUnpacker
extends Reference
extends RefCounted
# THIS CLASS TAKES INSPIRATION FROM PIXELORAMA'S FLOOD FILL
# AND HAS BEEN MODIFIED FOR OPTIMIZATION
var slice_thread := Thread.new()
var _include_boundary_threshold: int # the size of rect below which merging accounts for boundaty
var _merge_dist: int # after crossing threshold the smaller image will merge with larger image
# if it is within the _merge_dist
var _include_boundary_threshold: int ## Τhe size of rect below which merging accounts for boundary
## After crossing threshold the smaller image will merge with larger image
## if it is within the _merge_dist
var _merge_dist: int
# working array used as buffer for segments while flooding
var _allegro_flood_segments: Array
# results array per image while flooding
var _allegro_image_segments: Array
## Working array used as buffer for segments while flooding
var _allegro_flood_segments: Array[Segment]
## Results array per image while flooding
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:
@ -21,63 +44,58 @@ func _init(threshold: int, merge_dist: int) -> void:
_merge_dist = merge_dist
func get_used_rects(image: Image) -> Dictionary:
if OS.get_name() == "HTML5":
func get_used_rects(image: Image) -> RectData:
if ProjectSettings.get_setting("rendering/driver/threads/thread_model") != 2:
# Single-threaded mode
return get_rects(image)
else:
# If Thread model is set to "Multi-Threaded" in project settings>threads>thread model
if slice_thread.is_active():
else: # Multi-threaded mode
if slice_thread.is_started():
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:
return slice_thread.wait_to_finish()
else:
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:
# make a smaller image to make the loop shorter
var used_rect = image.get_used_rect()
if used_rect.size == Vector2.ZERO:
func get_rects(image: Image) -> RectData:
# Make a smaller image to make the loop shorter
var used_rect := image.get_used_rect()
if used_rect.size == Vector2i.ZERO:
return clean_rects([])
var test_image = image.get_rect(used_rect)
# prepare a bitmap to keep track of previous places
var test_image := image.get_region(used_rect)
# Prepare a bitmap to keep track of previous places
var scanned_area := BitMap.new()
scanned_area.create(test_image.get_size())
test_image.lock()
# Scan the image
var rects = []
var frame_size = Vector2.ZERO
var rects: Array[Rect2i] = []
var frame_size := Vector2i.ZERO
for y in test_image.get_size().y:
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 !scanned_area.get_bit(position):
if !scanned_area.get_bitv(position):
var rect := _estimate_rect(test_image, position)
scanned_area.set_bit_rect(rect, true)
rect.position += used_rect.position
rects.append(rect)
test_image.unlock()
var rects_info = clean_rects(rects)
rects_info["rects"].sort_custom(self, "sort_rects")
var rects_info := clean_rects(rects)
rects_info.rects.sort_custom(sort_rects)
return rects_info
func clean_rects(rects: Array) -> Dictionary:
var frame_size = Vector2.ZERO
func clean_rects(rects: Array[Rect2i]) -> RectData:
var frame_size := Vector2i.ZERO
for i in rects.size():
var target: Rect2 = rects.pop_front()
var test_rect = target
var target: Rect2i = rects.pop_front()
var test_rect := target
if (
target.size.x < _include_boundary_threshold
or target.size.y < _include_boundary_threshold
):
test_rect.size += Vector2(_merge_dist, _merge_dist)
test_rect.position -= Vector2(_merge_dist, _merge_dist) / 2
var merged = false
test_rect.size += Vector2i(_merge_dist, _merge_dist)
test_rect.position -= Vector2i(_merge_dist, _merge_dist) / 2
var merged := false
for rect_i in rects.size():
if test_rect.intersects(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
if target.size.y > frame_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)
if rect_a.end.y < rect_b.position.y:
return true
if rect_a.position.x < rect_b.position.x:
# if both lie in the same row
var start = rect_a.position
var size = Vector2(rect_b.end.x, rect_a.end.y)
if Rect2(start, size).intersects(rect_b):
var start := rect_a.position
var size := Vector2i(rect_b.end.x, rect_a.end.y)
if Rect2i(start, size).intersects(rect_b):
return true
return false
func _estimate_rect(image: Image, position: Vector2) -> Rect2:
func _estimate_rect(image: Image, position: Vector2) -> Rect2i:
var cel_image := Image.new()
cel_image.copy_from(image)
cel_image.lock()
var small_rect: Rect2 = _flood_fill(position, cel_image)
cel_image.unlock()
var small_rect := _flood_fill(position, cel_image)
return small_rect
# Add a new segment to the array
func _add_new_segment(y: int = 0) -> void:
var segment = {}
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)
## Add a new segment to the array
func _add_new_segment(y := 0) -> void:
_allegro_flood_segments.append(Segment.new(y))
# 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
# 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
# have been initialized
## 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
## line that has been filled.
## this method is called by `_flood_fill` after the required data structures
## have been initialized
func _flood_line_around_point(position: Vector2i, image: Image) -> int:
if not image.get_pixelv(position).a > 0:
return int(position.x) + 1
var west: Vector2 = position
var east: Vector2 = position
return position.x + 1
var west := position
var east := position
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:
east += Vector2.RIGHT
east += Vector2i.RIGHT
# Make a note of the stuff we processed
var c = int(position.y)
var segment = _allegro_flood_segments[c]
var c := position.y
var segment := _allegro_flood_segments[c]
# we may have already processed some segments on this y coordinate
if segment.flooding:
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
c = _allegro_flood_segments.size()
segment.next = c
_add_new_segment(int(position.y))
_add_new_segment(position.y)
segment = _allegro_flood_segments[c]
# set the values for the current segment
segment.flooding = true
@ -177,28 +185,28 @@ func _flood_line_around_point(position: Vector2, image: Image) -> int:
_allegro_image_segments.append(segment)
# 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
return int(east.x) + 1
return east.x + 1
func _check_flooded_segment(y: int, left: int, right: int, image: Image) -> bool:
var ret = false
var c: int = 0
var ret := false
var c := 0
while left <= right:
c = y
while true:
var segment = _allegro_flood_segments[c]
var segment := _allegro_flood_segments[c]
if left >= segment.left_position and left <= segment.right_position:
left = segment.right_position + 2
break
c = segment.next
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
break
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
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
# 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
# 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.fill(Color.transparent)
final_image.lock()
final_image.fill(Color.TRANSPARENT)
_select_segments(final_image)
final_image.unlock()
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
for j in image.get_height():
_add_new_segment(j)
@ -228,9 +234,9 @@ func _compute_segments_for_image(position: Vector2, image: Image) -> void:
var done := false
while not done:
done = true
var max_index = _allegro_flood_segments.size()
var max_index := _allegro_flood_segments.size()
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?
p.todo_below = false
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:
# short circuit for flat colors
for c in _allegro_image_segments.size():
var p = _allegro_image_segments[c]
var rect = Rect2()
rect.position = Vector2(p.left_position, p.y)
rect.end = Vector2(p.right_position + 1, p.y + 1)
map.fill_rect(rect, Color.white)
var p := _allegro_image_segments[c]
var rect := Rect2i()
rect.position = Vector2i(p.left_position, p.y)
rect.end = Vector2i(p.right_position + 1, p.y + 1)
map.fill_rect(rect, Color.WHITE)

View file

@ -13,7 +13,7 @@ func export_animation(
progress_report_obj: Object,
progress_report_method,
progress_report_args
) -> PoolByteArray:
) -> PackedByteArray:
var frame_count := len(frames)
var result := AImgIOAPNGStream.new()
# Magic number
@ -65,7 +65,7 @@ func export_animation(
# setup chunk interior...
var ichk := result.start_chunk()
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
if i == 0:
result.write_chunk("IDAT", chunk.data_array)
@ -74,7 +74,7 @@ func export_animation(
# Done with this frame!
progress_report_obj.callv(progress_report_method, progress_report_args)
# Final chunk.
result.write_chunk("IEND", PoolByteArray())
result.write_chunk("IEND", PackedByteArray())
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.
# But you should always get perfection for integers.
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
var fallback := 10000
while num > 32767:
@ -123,7 +123,7 @@ func write_padded_lines(sp: StreamPeer, img: Image):
var base := 0
while y < h:
var nl := base + (w * 4)
var line := data.subarray(base, nl - 1)
var line := data.slice(base, nl)
sp.put_8(0)
sp.put_data(line)
y += 1

View file

@ -1,80 +1,60 @@
tool
@tool
class_name AImgIOAPNGImportPlugin
extends EditorImportPlugin
func get_importer_name() -> String:
func _get_importer_name() -> String:
return "aimgio.apng_animatedtexture"
func get_visible_name() -> String:
func _get_visible_name() -> String:
return "APNG as AnimatedTexture"
func get_save_extension() -> String:
func _get_save_extension() -> String:
return "res"
func get_resource_type() -> String:
func _get_resource_type() -> String:
return "AnimatedTexture"
func get_recognized_extensions() -> Array:
return ["png"]
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(["png"])
func get_preset_count():
func _get_preset_count():
return 1
func get_preset_name(_i):
func _get_preset_name(_i):
return "Default"
func get_import_options(_i):
# GDLint workaround - it really does not want this string to exist due to length.
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_import_options(_path: String, _i: int) -> Array[Dictionary]:
return []
func get_option_visibility(_option, _options):
func _get_import_order():
return 0
func _get_option_visibility(_path: String, _option, _options):
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)
if res[0] != null:
push_error("AImgIOPNGImporter: " + res[0])
return ERR_FILE_UNRECOGNIZED
var frames: Array = res[1]
var root: AnimatedTexture = AnimatedTexture.new()
var flags: int = options["texture_flags"]
flags |= options["texture_flags_add"]
root.flags = flags
root.frames = len(frames)
root.fps = 1
for i in range(len(frames)):
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()
tx.storage = options["image_texture_storage"]
tx.lossy_quality = options["image_texture_lossy_quality"]
tx.create_from_image(f.content, flags)
tx.create_from_image(f.content)
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
extends Reference
extends RefCounted
# 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
# Therefore it just uses the rule:
@ -10,7 +10,7 @@ extends Reference
# Imports an APNG PoolByteArray into an animation as an Array of frames.
# Returns [error, frames] similar to some read functions.
# 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 magic_str = stream.read_magic()
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:
# IHDR (modified), PLTE (if present), tRNS (if present), IDAT (from fdAT),
# and IEND (generated).
var ihdr := PoolByteArray()
var plte := PoolByteArray()
var trns := PoolByteArray()
var ihdr := PackedByteArray()
var plte := PackedByteArray()
var trns := PackedByteArray()
# stored full width/height for buffer
var width := 0
var height := 0
# parse chunks
var frames := []
var frames: Array[BFrame] = []
while stream.read_chunk() == OK:
if stream.chunk_type == "IHDR":
ihdr = stream.chunk_data
@ -78,17 +78,16 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
if len(frames) > 0:
var f: BFrame = frames[len(frames) - 1]
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)
# theoretically we *could* store the default frame somewhere, but *why*?
# just use Image functions if you want that
if len(frames) == 0:
return ["No frames", null]
# prepare initial operating buffer
var operating := Image.new()
operating.create(width, height, false, Image.FORMAT_RGBA8)
var operating := Image.create(width, height, false, Image.FORMAT_RGBA8)
operating.fill(Color(0, 0, 0, 0))
var finished := []
var finished: Array[AImgIOFrame] = []
for v in frames:
var fv: BFrame = v
# 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 copy_blit_target := true
# rectangles and such
var blit_src := Rect2(Vector2.ZERO, intermediary_img.get_size())
var blit_pos := Vector2(fv.x, fv.y)
var blit_tgt := Rect2(blit_pos, intermediary_img.get_size())
var blit_src := Rect2i(Vector2i.ZERO, intermediary_img.get_size())
var blit_pos := Vector2i(fv.x, fv.y)
var blit_tgt := Rect2i(blit_pos, intermediary_img.get_size())
# early dispose ops
if fv.dispose_op == 2:
# previous
@ -118,6 +117,7 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
blit_target.copy_from(operating)
copy_blit_target = false
# actually blit
if blit_src.size != Vector2i.ZERO:
if fv.blend_op == 0:
blit_target.blit_rect(intermediary_img, blit_src, blit_pos)
else:
@ -143,10 +143,10 @@ static func load_from_buffer(buffer: PoolByteArray) -> Array:
# Imports an APNG file into an animation as an array of frames.
# Returns null on error.
static func load_from_file(path: String) -> Array:
var o := File.new()
if o.open(path, File.READ) != OK:
var o := FileAccess.open(path, FileAccess.READ)
if o == null:
return [null, "Unable to open file: " + path]
var l = o.get_len()
var l = o.get_length()
var data = o.get_buffer(l)
o.close()
return load_from_buffer(data)
@ -154,7 +154,7 @@ static func load_from_file(path: String) -> Array:
# Intermediate frame structure
class BFrame:
extends Reference
extends RefCounted
var dispose_op: int
var blend_op: int
var x: int
@ -162,9 +162,9 @@ class BFrame:
var w: int
var h: int
var duration: float
var data: PoolByteArray
var data: PackedByteArray
func setup(fctl: PoolByteArray):
func setup(fctl: PackedByteArray):
if len(fctl) < 26:
return ""
var sp := StreamPeerBuffer.new()
@ -191,8 +191,8 @@ class BFrame:
# This can be loaded by Godot directly.
# This basically skips most of the APNG decoding process.
func intermediary(
ihdr: PoolByteArray, plte: PoolByteArray, trns: PoolByteArray
) -> PoolByteArray:
ihdr: PackedByteArray, plte: PackedByteArray, trns: PackedByteArray
) -> PackedByteArray:
# Might be important to note this operates on a copy of ihdr (by-value).
var sp := StreamPeerBuffer.new()
sp.data_array = ihdr
@ -207,8 +207,8 @@ class BFrame:
if len(trns) > 0:
intermed.write_chunk("tRNS", trns)
intermed.write_chunk("IDAT", data)
intermed.write_chunk("IEND", PoolByteArray())
intermed.write_chunk("IEND", PackedByteArray())
return intermed.finish()
func add_data(d: PoolByteArray):
func add_data(d: PackedByteArray):
data.append_array(d)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,15 @@
tool
@tool
extends TabContainer
signal tab_layout_changed(tab)
const ReferenceControl = preload("dockable_panel_reference_control.gd")
const Layout = preload("layout.gd")
var leaf: DockableLayoutPanel:
get:
return get_leaf()
set(value):
set_leaf(value)
var leaf: Layout.LayoutPanel setget set_leaf, get_leaf
var _leaf: Layout.LayoutPanel
var _leaf: DockableLayoutPanel
func _ready() -> void:
@ -16,49 +17,51 @@ func _ready() -> void:
func _enter_tree() -> void:
connect("tab_selected", self, "_on_tab_selected")
connect("tab_changed", self, "_on_tab_changed")
active_tab_rearranged.connect(_on_tab_changed)
tab_selected.connect(_on_tab_selected)
tab_changed.connect(_on_tab_changed)
func _exit_tree() -> void:
disconnect("tab_selected", self, "_on_tab_selected")
disconnect("tab_changed", self, "_on_tab_changed")
active_tab_rearranged.disconnect(_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
var min_size = min(nodes.size(), get_child_count())
var min_size := mini(nodes.size(), get_child_count())
# remove spare children
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
remove_child(child)
child.queue_free()
# add missing children
for i in range(min_size, nodes.size()):
var ref_control = ReferenceControl.new()
var ref_control := DockableReferenceControl.new()
add_child(ref_control)
assert(nodes.size() == get_child_count(), "FIXME")
# setup children
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]
set_tab_title(i, nodes[i].name)
set_leaf(new_leaf)
func get_child_rect() -> Rect2:
var control = get_current_tab_control()
return Rect2(rect_position + control.rect_position, control.rect_size)
var control := get_current_tab_control()
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:
current_tab = clamp(value.current_tab, 0, get_tab_count() - 1)
current_tab = clampi(value.current_tab, 0, get_tab_count() - 1)
_leaf = value
func get_leaf() -> Layout.LayoutPanel:
func get_leaf() -> DockableLayoutPanel:
return _leaf
@ -74,10 +77,10 @@ func _on_tab_selected(tab: int) -> void:
func _on_tab_changed(tab: int) -> void:
if not _leaf:
return
var control = get_tab_control(tab)
var control := get_tab_control(tab)
if not control:
return
var tab_name = control.name
var name_index_in_leaf = _leaf.find_name(tab_name)
var tab_name := control.name
var name_index_in_leaf := _leaf.find_name(tab_name)
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
# 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
func _ready() -> void:
mouse_filter = MOUSE_FILTER_IGNORE
set_notify_transform(true)
func _notification(what: int) -> void:
if what == NOTIFICATION_VISIBILITY_CHANGED and _reference_to:
_reference_to.visible = visible
elif what == NOTIFICATION_SORT_CHILDREN and _reference_to:
elif what == NOTIFICATION_TRANSFORM_CHANGED and _reference_to:
_reposition_reference()
@ -22,27 +40,9 @@ func _get_minimum_size() -> Vector2:
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:
_reference_to.rect_global_position = rect_global_position
_reference_to.rect_size = rect_size
_reference_to.global_position = global_position
_reference_to.size = size
func _on_reference_to_renamed() -> void:

View file

@ -1,45 +1,48 @@
tool
@tool
extends Control
const DRAW_NOTHING = -1
const DRAW_CENTERED = -2
enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER }
var _draw_margin = DRAW_NOTHING
var _should_split = false
const DRAW_NOTHING := -1
const DRAW_CENTERED := -2
const MARGIN_NONE := -1
var _draw_margin := DRAW_NOTHING
var _should_split := false
func _notification(what: int) -> void:
if what == NOTIFICATION_MOUSE_EXIT:
_draw_margin = DRAW_NOTHING
update()
queue_redraw()
elif what == NOTIFICATION_MOUSE_ENTER and not _should_split:
_draw_margin = DRAW_CENTERED
update()
queue_redraw()
func _gui_input(event: InputEvent) -> void:
if _should_split and event is InputEventMouseMotion:
_draw_margin = _find_hover_margin(event.position)
update()
queue_redraw()
func _draw() -> void:
var rect
var rect: Rect2
if _draw_margin == DRAW_NOTHING:
return
elif _draw_margin == DRAW_CENTERED:
rect = Rect2(Vector2.ZERO, rect_size)
rect = Rect2(Vector2.ZERO, size)
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:
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:
var half_width = rect_size.x * 0.5
rect = Rect2(half_width, 0, half_width, rect_size.y)
var half_width = size.x * 0.5
rect = Rect2(half_width, 0, half_width, size.y)
elif _draw_margin == MARGIN_BOTTOM:
var half_height = rect_size.y * 0.5
rect = Rect2(0, half_height, rect_size.x, half_height)
var stylebox = get_stylebox("panel", "TooltipPanel")
var half_height = size.y * 0.5
rect = Rect2(0, half_height, size.x, half_height)
var stylebox := get_theme_stylebox("panel", "TooltipPanel")
draw_style_box(stylebox, rect)
@ -48,7 +51,7 @@ func set_enabled(enabled: bool, should_split: bool = true) -> void:
_should_split = should_split
if enabled:
_draw_margin = DRAW_NOTHING
update()
queue_redraw()
func get_hover_margin() -> int:
@ -56,23 +59,23 @@ func get_hover_margin() -> 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 lesser = left
var lesser_margin = MARGIN_LEFT
var left := point.distance_squared_to(Vector2(0, half_size.y))
var lesser := 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:
lesser = 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:
lesser = 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:
#lesser = bottom # unused result
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
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
func parse_property(
_object: Object, _type: int, path: String, _hint: int, _hint_text: String, _usage: int
func _parse_property(
_object: Object,
_type: Variant.Type,
name: String,
_hint: PropertyHint,
_hint_text: String,
_usage: int,
_wide: bool
) -> bool:
if path == "layout":
var editor_property = LayoutEditorProperty.new()
if name == "layout":
var editor_property := LayoutEditorProperty.new()
add_property_editor("layout", editor_property)
return false

View file

@ -1,71 +1,71 @@
extends EditorProperty
const DockableContainer = preload("../dockable_container.gd")
const Layout = preload("../layout.gd")
var _container = DockableContainer.new()
var _hidden_menu_button = MenuButton.new()
var _container := DockableContainer.new()
var _hidden_menu_button := MenuButton.new()
var _hidden_menu_popup: PopupMenu
var _hidden_menu_list: PoolStringArray
var _hidden_menu_list: PackedStringArray
func _ready() -> void:
rect_min_size = Vector2(128, 256)
custom_minimum_size = Vector2(128, 256)
_hidden_menu_button.text = "Visible nodes"
add_child(_hidden_menu_button)
_hidden_menu_popup = _hidden_menu_button.get_popup()
_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.connect("id_pressed", self, "_on_hidden_menu_popup_id_pressed")
_hidden_menu_popup.about_to_popup.connect(_on_hidden_menu_popup_about_to_show)
_hidden_menu_popup.id_pressed.connect(_on_hidden_menu_popup_id_pressed)
_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 = original_container.get(get_edited_property())
_container.set(get_edited_property(), value)
var value := _get_layout().clone() # The layout gets reset when selecting it without clone
for n in value.get_names():
var child = _create_child_control(n)
var child := _create_child_control(n)
_container.add_child(child)
_container.set(get_edited_property(), value)
add_child(_container)
set_bottom_editor(_container)
func update_property() -> void:
var value = _get_layout()
func _exit_tree() -> void: # Not sure if this is needed, but just to be sure
queue_free()
func _update_property() -> void:
var value := _get_layout()
_container.set(get_edited_property(), value)
func _get_layout() -> Layout:
var original_container: DockableContainer = get_edited_object()
func _get_layout() -> DockableLayout:
var original_container := get_edited_object() as DockableContainer
return original_container.get(get_edited_property())
func _create_child_control(named: String) -> Control:
var new_control = Label.new()
func _create_child_control(named: String) -> Label:
var new_control := Label.new()
new_control.name = named
new_control.align = Label.ALIGN_CENTER
new_control.valign = Label.VALIGN_CENTER
new_control.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
new_control.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
new_control.clip_text = true
new_control.text = named
return new_control
func _on_hidden_menu_popup_about_to_show() -> void:
var layout = _get_layout()
var layout := _get_layout().clone()
_hidden_menu_popup.clear()
_hidden_menu_list = layout.get_names()
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.set_item_checked(i, not layout.is_tab_hidden(tab_name))
func _on_hidden_menu_popup_id_pressed(id: int) -> void:
var layout = _get_layout()
var tab_name = _hidden_menu_list[id]
var new_hidden = not layout.is_tab_hidden(tab_name)
layout.set_tab_hidden(tab_name, new_hidden)
var layout := _get_layout().clone()
var tab_name := _hidden_menu_list[id]
var new_hidden := not layout.is_tab_hidden(tab_name)
_get_layout().set_tab_hidden(tab_name, 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
# Layout Resource definition, holding the root LayoutNode and hidden tabs.
#
# LayoutSplit are binary trees with nested LayoutSplit subtrees and LayoutPanel
# leaves. Both of them inherit from LayoutNode to help with type annotation and
# define common funcionality.
#
# Hidden tabs are marked in the `hidden_tabs` Dictionary by name.
## DockableLayout Resource definition, holding the root DockableLayoutNode and hidden tabs.
##
## DockableLayoutSplit are binary trees with nested DockableLayoutSplit subtrees
## and DockableLayoutPanel leaves. Both of them inherit from DockableLayoutNode to help with
## type annotation and define common functionality.
##
## Hidden tabs are marked in the `hidden_tabs` Dictionary by name.
const LayoutNode = preload("layout_node.gd")
const LayoutPanel = preload("layout_panel.gd")
const LayoutSplit = preload("layout_split.gd")
enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER }
export(Resource) var root = LayoutPanel.new() setget set_root, get_root
export(Dictionary) var hidden_tabs = {} setget set_hidden_tabs, get_hidden_tabs
@export var root: DockableLayoutNode = DockableLayoutPanel.new():
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 _first_leaf: LayoutPanel
var _changed_signal_queued := false
var _first_leaf: DockableLayoutPanel
var _hidden_tabs: Dictionary
var _leaf_by_node_name: Dictionary
var _root: LayoutNode = LayoutPanel.new()
var _root: DockableLayoutNode = DockableLayoutPanel.new()
func _init() -> void:
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:
value = LayoutPanel.new()
value = DockableLayoutPanel.new()
if _root == value:
return
if _root and _root.is_connected("changed", self, "_on_root_changed"):
_root.disconnect("changed", self, "_on_root_changed")
if _root and _root.changed.is_connected(_on_root_changed):
_root.changed.disconnect(_on_root_changed)
_root = value
_root.parent = null
_root.connect("changed", self, "_on_root_changed")
_root.changed.connect(_on_root_changed)
if should_emit_changed:
_on_root_changed()
func get_root() -> LayoutNode:
func get_root() -> DockableLayoutNode:
return _root
func set_hidden_tabs(value: Dictionary) -> void:
if value != _hidden_tabs:
_hidden_tabs = value
emit_signal("changed")
func clone() -> DockableLayout:
return duplicate(true)
func get_hidden_tabs() -> Dictionary:
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:
func get_names() -> PackedStringArray:
return _root.get_names()
# Add missing nodes on first leaf and remove nodes outside indices from leaves.
#
# _leaf_by_node_name = {
# (string keys) = respective Leaf that holds the node name,
# }
func update_nodes(names: PoolStringArray) -> void:
## Add missing nodes on first leaf and remove nodes outside indices from leaves.
##
## _leaf_by_node_name = {
## (string keys) = respective Leaf that holds the node name,
## }
func update_nodes(names: PackedStringArray) -> void:
_leaf_by_node_name.clear()
_first_leaf = null
var empty_leaves = []
_ensure_names_in_node(_root, names, empty_leaves)
var empty_leaves: Array[DockableLayoutPanel] = []
_ensure_names_in_node(_root, names, empty_leaves) # Changes _leaf_by_node_name and empty_leaves
for l in empty_leaves:
_remove_leaf(l)
if not _first_leaf:
_first_leaf = LayoutPanel.new()
_first_leaf = DockableLayoutPanel.new()
set_root(_first_leaf)
for n in names:
if not _leaf_by_node_name.has(n):
@ -87,12 +83,12 @@ func update_nodes(names: PoolStringArray) -> void:
_on_root_changed()
func move_node_to_leaf(node: Node, leaf: LayoutPanel, relative_position: int) -> void:
var node_name = node.name
var previous_leaf = _leaf_by_node_name.get(node_name)
func move_node_to_leaf(node: Node, leaf: DockableLayoutPanel, relative_position: int) -> void:
var node_name := node.name
var previous_leaf: DockableLayoutPanel = _leaf_by_node_name.get(node_name)
if previous_leaf:
previous_leaf.remove_node(node)
if previous_leaf.empty():
if previous_leaf.is_empty():
_remove_leaf(previous_leaf)
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()
func get_leaf_for_node(node: Node) -> LayoutPanel:
func get_leaf_for_node(node: Node) -> DockableLayoutPanel:
return _leaf_by_node_name.get(node.name)
func split_leaf_with_node(leaf, node: Node, margin: int) -> void:
var root_branch = leaf.parent
var new_leaf = LayoutPanel.new()
var new_branch = LayoutSplit.new()
func split_leaf_with_node(leaf: DockableLayoutPanel, node: Node, margin: int) -> void:
var root_branch := leaf.parent
var new_leaf := DockableLayoutPanel.new()
var new_branch := DockableLayoutSplit.new()
if margin == MARGIN_LEFT or margin == MARGIN_RIGHT:
new_branch.direction = LayoutSplit.Direction.HORIZONTAL
new_branch.direction = DockableLayoutSplit.Direction.HORIZONTAL
else:
new_branch.direction = LayoutSplit.Direction.VERTICAL
new_branch.direction = DockableLayoutSplit.Direction.VERTICAL
if margin == MARGIN_LEFT or margin == MARGIN_TOP:
new_branch.first = new_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:
var node_name = node.name
var node_name := node.name
if _leaf_by_node_name.has(node_name):
return
_first_leaf.push_name(node_name)
@ -139,19 +135,19 @@ func add_node(node: Node) -> void:
func remove_node(node: Node) -> void:
var node_name = node.name
var leaf: LayoutPanel = _leaf_by_node_name.get(node_name)
var node_name := node.name
var leaf: DockableLayoutPanel = _leaf_by_node_name.get(node_name)
if not leaf:
return
leaf.remove_node(node)
_leaf_by_node_name.erase(node_name)
if leaf.empty():
if leaf.is_empty():
_remove_leaf(leaf)
_on_root_changed()
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:
return
leaf.rename_node(previous_name, new_name)
@ -187,35 +183,35 @@ func _on_root_changed() -> void:
return
_changed_signal_queued = true
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:
if node is LayoutPanel:
node.update_nodes(names, _leaf_by_node_name)
if node.empty():
func _ensure_names_in_node(
node: DockableLayoutNode, names: PackedStringArray, empty_leaves: Array[DockableLayoutPanel]
) -> void:
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)
if not _first_leaf:
_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.second, names, empty_leaves)
else:
assert(false, "Invalid Resource, should be branch or leaf, found %s" % node)
func _remove_leaf(leaf: LayoutPanel) -> void:
assert(leaf.empty(), "FIXME: trying to remove a leaf with nodes")
func _remove_leaf(leaf: DockableLayoutPanel) -> void:
assert(leaf.is_empty(), "FIXME: trying to remove_at a leaf with nodes")
if _root == leaf:
return
var collapsed_branch = leaf.parent
assert(collapsed_branch is LayoutSplit, "FIXME: leaf is not a child of branch")
var kept_branch = (
collapsed_branch.first
if leaf == collapsed_branch.second
else collapsed_branch.second
var collapsed_branch := leaf.parent
assert(collapsed_branch is DockableLayoutSplit, "FIXME: leaf is not a child of branch")
var kept_branch: DockableLayoutNode = (
collapsed_branch.first 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:
set_root(kept_branch, true)
elif root_branch:
@ -231,10 +227,10 @@ func _print_tree() -> void:
print("")
func _print_tree_step(tree_or_leaf, level, idx) -> void:
if tree_or_leaf is LayoutPanel:
func _print_tree_step(tree_or_leaf: DockableLayoutNode, level: int, idx: int) -> void:
if tree_or_leaf is DockableLayoutPanel:
print(" |".repeat(level), "- (%d) = " % idx, tree_or_leaf.names)
else:
elif tree_or_leaf is DockableLayoutSplit:
print(
" |".repeat(level),
"-+ (%d) = " % idx,

View file

@ -1,30 +1,23 @@
tool
@tool
class_name DockableLayoutNode
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:
var node = self
var node := self
while node:
node.emit_signal("changed")
node.emit_changed()
node = node.parent
# Returns a deep copy of the layout.
#
# 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:
## Returns whether there are any nodes
func is_empty() -> bool:
return true
# Returns all tab names in this node
func get_names() -> PoolStringArray:
return PoolStringArray()
## Returns all tab names in this node
func get_names() -> PackedStringArray:
return PackedStringArray()

View file

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

View file

@ -1,76 +1,77 @@
tool
extends "layout_node.gd"
# Layout binary tree nodes, defining subtrees and leaf panels
@tool
class_name DockableLayoutSplit
extends DockableLayoutNode
## DockableLayout binary tree nodes, defining subtrees and leaf panels
enum Direction {
HORIZONTAL,
VERTICAL,
}
enum Direction { 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
export(float, 0, 1) var percent = 0.5 setget set_percent, get_percent
export(Resource) var first = LayoutPanel.new() setget set_first, get_first
export(Resource) var second = LayoutPanel.new() setget set_second, get_second
var _direction = Direction.HORIZONTAL
var _percent = 0.5
var _first
var _second
var _direction := Direction.HORIZONTAL
var _percent := 0.5
var _first: DockableLayoutNode
var _second: DockableLayoutNode
func _init() -> void:
resource_name = "Split"
func clone():
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:
func set_first(value: DockableLayoutNode) -> void:
if value == null:
_first = LayoutPanel.new()
_first = DockableLayoutPanel.new()
else:
_first = value
_first.parent = self
emit_tree_changed()
func get_first():
func get_first() -> DockableLayoutNode:
return _first
func set_second(value) -> void:
func set_second(value: DockableLayoutNode) -> void:
if value == null:
_second = LayoutPanel.new()
_second = DockableLayoutPanel.new()
else:
_second = value
_second.parent = self
emit_tree_changed()
func get_second():
func get_second() -> DockableLayoutNode:
return _second
func set_direction(value: int) -> void:
func set_direction(value: Direction) -> void:
if value != _direction:
_direction = value
emit_tree_changed()
func get_direction() -> int:
func get_direction() -> Direction:
return _direction
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):
_percent = clamped_value
emit_tree_changed()
@ -80,14 +81,15 @@ func get_percent() -> float:
return _percent
func get_names() -> PoolStringArray:
var names = _first.get_names()
func get_names() -> PackedStringArray:
var names := _first.get_names()
names.append_array(_second.get_names())
return names
func empty() -> bool:
return _first.empty() and _second.empty()
## Returns whether there are any nodes
func is_empty() -> bool:
return _first.is_empty() and _second.is_empty()
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."
author="gilzoide"
version="1.1.1"
version="1.1.2"
script="plugin.gd"

View file

@ -1,15 +1,15 @@
tool
@tool
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:
_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)

View file

@ -1,10 +1,10 @@
extends VBoxContainer
const SAVED_LAYOUT_PATH = "user://layout.tres"
const SAVED_LAYOUT_PATH := "user://layout.tres"
onready var _container = $DockableContainers/DockableContainer
onready var _clone_control = $HBoxContainer/ControlPrefab
onready var _checkbox_container = $HBoxContainer
@onready var _container := $DockableContainers/DockableContainer as DockableContainer
@onready var _clone_control := $HBoxContainer/ControlPrefab as ColorRect
@onready var _checkbox_container := $HBoxContainer as HBoxContainer
func _ready() -> void:
@ -12,33 +12,33 @@ func _ready() -> void:
$HBoxContainer/SaveLayoutButton.visible = false
$HBoxContainer/LoadLayoutButton.visible = false
var tabs = _container.get_tabs()
var tabs := _container.get_tabs()
for i in tabs.size():
var checkbox = CheckBox.new()
var checkbox := CheckBox.new()
checkbox.text = str(i)
checkbox.pressed = not _container.is_control_hidden(tabs[i])
checkbox.connect("toggled", self, "_on_CheckButton_toggled", [tabs[i]])
checkbox.button_pressed = not _container.is_control_hidden(tabs[i])
checkbox.toggled.connect(_on_CheckButton_toggled.bind(tabs[i]))
_checkbox_container.add_child(checkbox)
func _on_add_pressed() -> void:
var control = _clone_control.duplicate()
control.get_node("Buttons/Rename").connect(
"pressed", self, "_on_control_rename_button_pressed", [control]
var control := _clone_control.duplicate()
control.get_node("Buttons/Rename").pressed.connect(
_on_control_rename_button_pressed.bind(control)
)
control.get_node("Buttons/Remove").connect(
"pressed", self, "_on_control_remove_button_pressed", [control]
control.get_node("Buttons/Remove").pressed.connect(
_on_control_remove_button_pressed.bind(control)
)
control.color = Color(randf(), randf(), randf())
control.name = "Control0"
_container.add_child(control, true)
yield(_container, "sort_children")
await _container.sort_children
_container.set_control_as_current_tab(control)
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")
@ -51,11 +51,11 @@ func _on_load_pressed() -> 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:
_container.remove_child(control)
control.get_parent().remove_child(control)
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 path="res://addons/dockable_container/layout.gd" type="Script" id=2]
[ext_resource path="res://addons/dockable_container/layout_split.gd" type="Script" id=3]
[ext_resource path="res://addons/dockable_container/samples/TestScene.gd" type="Script" id=4]
[ext_resource path="res://addons/dockable_container/layout_panel.gd" type="Script" id=5]
[ext_resource type="Script" path="res://addons/dockable_container/dockable_container.gd" id="1"]
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="2"]
[ext_resource type="Script" path="res://addons/dockable_container/samples/TestScene.gd" id="4"]
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="4_yhgfb"]
[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"
script = ExtResource( 5 )
names = PoolStringArray( "Control0" )
script = ExtResource("5")
names = PackedStringArray("Control0")
current_tab = 0
[sub_resource type="Resource" id=1]
[sub_resource type="Resource" id="Resource_6kjom"]
resource_name = "Tabs"
script = ExtResource( 5 )
names = PoolStringArray( "Control1", "Control2" )
script = ExtResource("5")
names = PackedStringArray("Control1", "Control2")
current_tab = 0
[sub_resource type="Resource" id=6]
[sub_resource type="Resource" id="Resource_hl8y1"]
resource_name = "Split"
script = ExtResource( 3 )
script = ExtResource("4_yhgfb")
direction = 1
percent = 0.5
first = SubResource( 5 )
second = SubResource( 1 )
first = SubResource("Resource_8aoc2")
second = SubResource("Resource_6kjom")
[sub_resource type="Resource" id=2]
[sub_resource type="Resource" id="Resource_ybwqe"]
resource_name = "Layout"
script = ExtResource( 2 )
root = SubResource( 6 )
hidden_tabs = {
}
script = ExtResource("2")
root = SubResource("Resource_hl8y1")
hidden_tabs = {}
[sub_resource type="Resource" id=3]
[sub_resource type="Resource" id="Resource_ntwfj"]
resource_name = "Tabs"
script = ExtResource( 5 )
names = PoolStringArray( "Control3" )
script = ExtResource("5")
names = PackedStringArray("Control3")
current_tab = 0
[sub_resource type="Resource" id=7]
[sub_resource type="Resource" id="Resource_dmyvf"]
resource_name = "Tabs"
script = ExtResource( 5 )
names = PoolStringArray( "Control4" )
script = ExtResource("5")
names = PackedStringArray("Control4")
current_tab = 0
[sub_resource type="Resource" id=8]
[sub_resource type="Resource" id="Resource_vag66"]
resource_name = "Split"
script = ExtResource( 3 )
script = ExtResource("4_yhgfb")
direction = 1
percent = 0.28125
first = SubResource( 3 )
second = SubResource( 7 )
percent = 0.281
first = SubResource("Resource_ntwfj")
second = SubResource("Resource_dmyvf")
[sub_resource type="Resource" id=9]
[sub_resource type="Resource" id="Resource_4q660"]
resource_name = "Tabs"
script = ExtResource( 5 )
names = PoolStringArray( "Control5" )
script = ExtResource("5")
names = PackedStringArray("Control5")
current_tab = 0
[sub_resource type="Resource" id=10]
[sub_resource type="Resource" id="Resource_jhibs"]
resource_name = "Split"
script = ExtResource( 3 )
script = ExtResource("4_yhgfb")
direction = 0
percent = 0.5
first = SubResource( 8 )
second = SubResource( 9 )
first = SubResource("Resource_vag66")
second = SubResource("Resource_4q660")
[sub_resource type="Resource" id=4]
[sub_resource type="Resource" id="Resource_xhxpg"]
resource_name = "Layout"
script = ExtResource( 2 )
root = SubResource( 10 )
hidden_tabs = {
}
script = ExtResource("2")
root = SubResource("Resource_jhibs")
hidden_tabs = {}
[node name="SampleScene" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 4 )
script = ExtResource("4")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
margin_right = 1024.0
margin_bottom = 20.0
custom_constants/separation = 16
layout_mode = 2
alignment = 1
[node name="AddControlButton" type="Button" parent="HBoxContainer"]
margin_left = 345.0
margin_right = 472.0
margin_bottom = 20.0
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 4
text = "(+) ADD CONTROL"
[node name="SaveLayoutButton" type="Button" parent="HBoxContainer"]
margin_left = 488.0
margin_right = 575.0
margin_bottom = 20.0
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 4
text = "Save Layout"
[node name="LoadLayoutButton" type="Button" parent="HBoxContainer"]
margin_left = 591.0
margin_right = 679.0
margin_bottom = 20.0
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 4
text = "Load Layout"
[node name="ControlPrefab" type="ColorRect" parent="HBoxContainer"]
visible = false
margin_left = 677.0
margin_right = 697.0
margin_bottom = 20.0
rect_min_size = Vector2( 20, 20 )
color = Color( 0.129412, 0.121569, 0.121569, 1 )
layout_mode = 2
color = Color(0.129412, 0.121569, 0.121569, 1)
[node name="Buttons" type="VBoxContainer" parent="HBoxContainer/ControlPrefab"]
layout_mode = 0
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -65.5
margin_top = -22.0
margin_right = 65.5
margin_bottom = 22.0
__meta__ = {
"_edit_use_anchors_": false
}
offset_left = -65.5
offset_top = -22.0
offset_right = 65.5
offset_bottom = 22.0
[node name="Rename" type="Button" parent="HBoxContainer/ControlPrefab/Buttons"]
margin_right = 131.0
margin_bottom = 20.0
layout_mode = 2
text = "Rename"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Remove" type="Button" parent="HBoxContainer/ControlPrefab/Buttons"]
margin_top = 24.0
margin_right = 131.0
margin_bottom = 44.0
layout_mode = 2
text = "REMOVE"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="DockableContainers" type="HBoxContainer" parent="."]
margin_top = 24.0
margin_right = 1024.0
margin_bottom = 600.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="DockableContainer" type="Container" parent="DockableContainers"]
margin_right = 483.0
margin_bottom = 576.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
layout = SubResource( 2 )
script = ExtResource("1")
layout = SubResource("Resource_ybwqe")
[node name="Control0" type="ColorRect" parent="DockableContainers/DockableContainer"]
margin_left = 4.0
margin_top = 32.0
margin_right = 479.0
margin_bottom = 278.0
rect_min_size = Vector2( 64, 64 )
layout_mode = 2
[node name="Control1" type="ColorRect" parent="DockableContainers/DockableContainer"]
margin_left = 4.0
margin_top = 326.0
margin_right = 479.0
margin_bottom = 572.0
rect_min_size = Vector2( 128, 128 )
color = Color( 0.141176, 0.0745098, 0.603922, 1 )
layout_mode = 2
color = Color(0.141176, 0.0745098, 0.603922, 1)
[node name="Control2" type="ColorRect" parent="DockableContainers/DockableContainer"]
visible = false
margin_top = 294.0
margin_right = 64.0
margin_bottom = 358.0
rect_min_size = Vector2( 64, 64 )
color = Color( 0.533333, 0.380392, 0.380392, 1 )
layout_mode = 2
color = Color(0.533333, 0.380392, 0.380392, 1)
[node name="Separator" type="ColorRect" parent="DockableContainers"]
margin_left = 487.0
margin_right = 537.0
margin_bottom = 576.0
rect_min_size = Vector2( 50, 0 )
color = Color( 0, 0, 0, 1 )
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
color = Color(0, 0, 0, 1)
[node name="DockableContainer2" type="Container" parent="DockableContainers"]
margin_left = 541.0
margin_right = 1024.0
margin_bottom = 576.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
layout = SubResource( 4 )
script = ExtResource("1")
layout = SubResource("Resource_xhxpg")
[node name="Control3" type="ColorRect" parent="DockableContainers/DockableContainer2"]
margin_left = 4.0
margin_top = 32.0
margin_right = 231.5
margin_bottom = 152.0
rect_min_size = Vector2( 64, 64 )
color = Color( 0, 1, 0.905882, 1 )
layout_mode = 2
color = Color(0, 1, 0.905882, 1)
[node name="Control4" type="ColorRect" parent="DockableContainers/DockableContainer2"]
margin_left = 4.0
margin_top = 200.0
margin_right = 231.5
margin_bottom = 572.0
rect_min_size = Vector2( 128, 128 )
color = Color( 0, 0.698039, 0.0588235, 1 )
layout_mode = 2
color = Color(0, 0.698039, 0.0588235, 1)
[node name="Control5" type="ColorRect" parent="DockableContainers/DockableContainer2"]
margin_left = 251.5
margin_top = 32.0
margin_right = 479.0
margin_bottom = 572.0
rect_min_size = Vector2( 64, 64 )
color = Color( 1, 0.937255, 0, 1 )
layout_mode = 2
color = Color(1, 0.937255, 0, 1)
[connection signal="pressed" from="HBoxContainer/AddControlButton" to="." method="_on_add_pressed"]
[connection signal="pressed" from="HBoxContainer/SaveLayoutButton" to="." method="_on_save_pressed"]

View file

@ -1,53 +1,49 @@
tool
@tool
extends Control
const Layout = preload("layout.gd")
const SPLIT_THEME_CLASS = [
"HSplitContainer", # SPLIT_THEME_CLASS[LayoutSplit.Direction.HORIZONTAL]
"VSplitContainer", # SPLIT_THEME_CLASS[LayoutSplit.Direction.VERTICAL]
const SPLIT_THEME_CLASS: PackedStringArray = [
"HSplitContainer", # SPLIT_THEME_CLASS[DockableLayoutSplit.Direction.HORIZONTAL]
"VSplitContainer", # SPLIT_THEME_CLASS[DockableLayoutSplit.Direction.VERTICAL]
]
const SPLIT_MOUSE_CURSOR_SHAPE = [
Control.CURSOR_HSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[LayoutSplit.Direction.HORIZONTAL]
Control.CURSOR_VSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[LayoutSplit.Direction.VERTICAL]
const SPLIT_MOUSE_CURSOR_SHAPE: Array[Control.CursorShape] = [
Control.CURSOR_HSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[DockableLayoutSplit.Direction.HORIZONTAL]
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 second_minimum_size: Vector2
var _parent_rect
var _mouse_hovering = false
var _dragging = false
var _parent_rect: Rect2
var _mouse_hovering := false
var _dragging := false
func _draw() -> void:
var theme_class = SPLIT_THEME_CLASS[layout_split.direction]
var icon = get_icon("grabber", theme_class)
var autohide = bool(get_constant("autohide", theme_class))
var theme_class := SPLIT_THEME_CLASS[layout_split.direction]
var icon := get_theme_icon("grabber", theme_class)
var autohide := bool(get_theme_constant("autohide", theme_class))
if not icon or (autohide and not _mouse_hovering):
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:
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()
if event.doubleclick:
if event.double_click:
layout_split.percent = 0.5
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():
layout_split.percent = (
(mouse_in_parent.x - _parent_rect.position.x)
/ _parent_rect.size.x
(mouse_in_parent.x - _parent_rect.position.x) / _parent_rect.size.x
)
else:
layout_split.percent = (
(mouse_in_parent.y - _parent_rect.position.y)
/ _parent_rect.size.y
(mouse_in_parent.y - _parent_rect.position.y) / _parent_rect.size.y
)
@ -55,13 +51,13 @@ func _notification(what: int) -> void:
if what == NOTIFICATION_MOUSE_ENTER:
_mouse_hovering = true
set_split_cursor(true)
if bool(get_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
update()
if bool(get_theme_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
queue_redraw()
elif what == NOTIFICATION_MOUSE_EXIT:
_mouse_hovering = false
set_split_cursor(false)
if bool(get_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
update()
if bool(get_theme_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
queue_redraw()
elif what == NOTIFICATION_FOCUS_EXIT:
_dragging = false
@ -69,15 +65,15 @@ func _notification(what: int) -> void:
func get_layout_minimum_size() -> Vector2:
if not layout_split:
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():
return Vector2(
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:
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
)
@ -91,41 +87,34 @@ func set_split_cursor(value: bool) -> void:
func get_split_rects(rect: Rect2) -> Dictionary:
_parent_rect = rect
var separation = get_constant("separation", SPLIT_THEME_CLASS[layout_split.direction])
var origin = rect.position
var size = rect.size
var percent = layout_split.percent
var separation := get_theme_constant("separation", SPLIT_THEME_CLASS[layout_split.direction])
var origin := rect.position
var percent := layout_split.percent
if layout_split.is_horizontal():
var first_width = max((size.x - separation) * percent, first_minimum_size.x)
var split_offset = clamp(
size.x * percent - separation * 0.5,
var split_offset := clampf(
rect.size.x * percent - separation * 0.5,
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 {
"first": Rect2(origin.x, origin.y, split_offset, size.y),
"self": Rect2(origin.x + split_offset, origin.y, separation, size.y),
"second": Rect2(origin.x + split_offset + separation, origin.y, second_width, size.y),
"first": Rect2(origin.x, origin.y, split_offset, rect.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, rect.size.y),
}
else:
var first_height = max((size.y - separation) * percent, first_minimum_size.y)
var split_offset = clamp(
size.y * percent - separation * 0.5,
var split_offset := clampf(
rect.size.y * percent - separation * 0.5,
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 {
"first": Rect2(origin.x, origin.y, size.x, split_offset),
"self": Rect2(origin.x, origin.y + split_offset, size.x, separation),
"second": Rect2(origin.x, origin.y + split_offset + separation, size.x, second_height),
"first": Rect2(origin.x, origin.y, rect.size.x, split_offset),
"self": Rect2(origin.x, origin.y + split_offset, rect.size.x, separation),
"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
func get_indexed_datas(image: Image, colors: Array) -> PoolByteArray:
_shader = preload("./lookup_color.shader")
func get_indexed_datas(image: Image, colors: Array) -> PackedByteArray:
_shader = preload("./lookup_color.gdshader")
return _convert(image, colors)
func get_similar_indexed_datas(image: Image, colors: Array) -> PoolByteArray:
_shader = preload("./lookup_similar.shader")
func get_similar_indexed_datas(image: Image, colors: Array) -> PackedByteArray:
_shader = preload("./lookup_similar.gdshader")
return _convert(image, colors)
func _convert(image: Image, colors: Array) -> PoolByteArray:
var vp = VisualServer.viewport_create()
var canvas = VisualServer.canvas_create()
VisualServer.viewport_attach_canvas(vp, canvas)
VisualServer.viewport_set_size(vp, image.get_width(), image.get_height())
VisualServer.viewport_set_disable_3d(vp, true)
VisualServer.viewport_set_usage(vp, VisualServer.VIEWPORT_USAGE_2D)
VisualServer.viewport_set_hdr(vp, true)
VisualServer.viewport_set_active(vp, true)
func _convert(image: Image, colors: Array) -> PackedByteArray:
var vp := RenderingServer.viewport_create()
var canvas := RenderingServer.canvas_create()
RenderingServer.viewport_attach_canvas(vp, canvas)
RenderingServer.viewport_set_size(vp, image.get_width(), image.get_height())
RenderingServer.viewport_set_disable_3d(vp, true)
RenderingServer.viewport_set_active(vp, true)
var ci_rid = VisualServer.canvas_item_create()
VisualServer.viewport_set_canvas_transform(vp, canvas, Transform())
VisualServer.canvas_item_set_parent(ci_rid, canvas)
var texture = ImageTexture.new()
texture.create_from_image(image)
VisualServer.canvas_item_add_texture_rect(
ci_rid, Rect2(Vector2(0, 0), image.get_size()), texture
var ci_rid := RenderingServer.canvas_item_create()
RenderingServer.viewport_set_canvas_transform(vp, canvas, Transform3D())
RenderingServer.canvas_item_set_parent(ci_rid, canvas)
var texture := ImageTexture.create_from_image(image)
RenderingServer.canvas_item_add_texture_rect(
ci_rid, Rect2(Vector2.ZERO, image.get_size()), texture
)
var mat_rid = VisualServer.material_create()
VisualServer.material_set_shader(mat_rid, _shader.get_rid())
var lut = Image.new()
lut.create(256, 1, false, Image.FORMAT_RGB8)
var mat_rid := RenderingServer.material_create()
RenderingServer.material_set_shader(mat_rid, _shader.get_rid())
var lut := Image.create(256, 1, false, Image.FORMAT_RGB8)
lut.fill(Color8(colors[0][0], colors[0][1], colors[0][2]))
lut.lock()
for i in colors.size():
lut.set_pixel(i, 0, Color8(colors[i][0], colors[i][1], colors[i][2]))
var lut_tex = ImageTexture.new()
lut_tex.create_from_image(lut)
VisualServer.material_set_param(mat_rid, "lut", lut_tex)
VisualServer.canvas_item_set_material(ci_rid, mat_rid)
var lut_tex := ImageTexture.create_from_image(lut)
# Not sure why putting lut_tex is an array is needed, but without it, it doesn't work
RenderingServer.material_set_param(mat_rid, "lut", [lut_tex])
RenderingServer.canvas_item_set_material(ci_rid, mat_rid)
VisualServer.viewport_set_update_mode(vp, VisualServer.VIEWPORT_UPDATE_ONCE)
VisualServer.viewport_set_vflip(vp, true)
VisualServer.force_draw(false)
image = VisualServer.texture_get_data(VisualServer.viewport_get_texture(vp))
RenderingServer.viewport_set_update_mode(vp, RenderingServer.VIEWPORT_UPDATE_ONCE)
RenderingServer.force_draw(false)
image = RenderingServer.texture_2d_get(RenderingServer.viewport_get_texture(vp))
VisualServer.free_rid(vp)
VisualServer.free_rid(canvas)
VisualServer.free_rid(ci_rid)
VisualServer.free_rid(mat_rid)
RenderingServer.free_rid(vp)
RenderingServer.free_rid(canvas)
RenderingServer.free_rid(ci_rid)
RenderingServer.free_rid(mat_rid)
image.convert(Image.FORMAT_R8)
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 }
var little_endian = preload("./little_endian.gd").new()
var lzw = preload("./gif-lzw/lzw.gd").new()
var converter = preload("./converter.gd")
var little_endian := preload("./little_endian.gd").new()
var lzw := preload("./gif-lzw/lzw.gd").new()
var converter := preload("./converter.gd")
var last_color_table := []
var last_transparency_index := -1
# File data and Header
var data := PoolByteArray([])
var data := PackedByteArray([])
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])
func export_file_data() -> PoolByteArray:
return data + PoolByteArray([0x3b])
func export_file_data() -> PackedByteArray:
return data + PackedByteArray([0x3b])
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:
@ -53,18 +53,17 @@ func add_application_ext(app_iden: String, app_auth_code: String, _data: Array)
data.append(extension_introducer)
data.append(extension_label)
data.append(block_size)
data += app_iden.to_ascii()
data += app_auth_code.to_ascii()
data += app_iden.to_ascii_buffer()
data += app_auth_code.to_ascii_buffer()
data.append(_data.size())
data += PoolByteArray(_data)
data += PackedByteArray(_data)
data.append(0)
# finds the image color table. Stops if the size gets larger than 256.
func find_color_table(image: Image) -> Dictionary:
image.lock()
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):
var color: Array = [
@ -77,8 +76,6 @@ func find_color_table(image: Image) -> Dictionary:
result[color] = result.size()
if result.size() > 256:
break
image.unlock()
return result
@ -89,10 +86,11 @@ func find_transparency_color_index(color_table: Dictionary) -> int:
return -1
func colors_to_codes(img: Image, col_palette: Dictionary, transp_color_index: int) -> PoolByteArray:
img.lock()
var image_data: PoolByteArray = img.get_data()
var result: PoolByteArray = PoolByteArray([])
func colors_to_codes(
img: Image, col_palette: Dictionary, transp_color_index: int
) -> PackedByteArray:
var image_data: PackedByteArray = img.get_data()
var result: PackedByteArray = PackedByteArray([])
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]]
@ -106,7 +104,6 @@ func colors_to_codes(img: Image, col_palette: Dictionary, transp_color_index: in
result.append(0)
push_warning("colors_to_codes: color not found! [%d, %d, %d, %d]" % color)
img.unlock()
return result
@ -120,11 +117,11 @@ func make_proper_size(color_table: Array) -> Array:
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:
var result: PoolByteArray = PoolByteArray([])
func color_table_to_indexes(colors: Array) -> PackedByteArray:
var result: PackedByteArray = PackedByteArray([])
for i in range(colors.size()):
result.append(i)
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 image_converted_to_codes: PoolByteArray
var image_converted_to_codes: PackedByteArray
var transparency_color_index: int = -1
var color_table: Array
if found_color_table.size() <= 256: # we don't need to quantize the image.
# try to find transparency color index.
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.
if transparency_color_index == -1 and found_color_table.size() <= 255:
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(
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]
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
# adds frame with last color informations
## Adds frame with last color information
func add_frame_with_lci(image: Image, frame_delay: float) -> int:
# check if image is of good format
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():
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
)
@ -201,7 +198,7 @@ func add_frame_with_lci(image: Image, frame_delay: float) -> int:
var compressed_image_result: Array = lzw.compress_lzw(
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 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:
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 proper_size := int(pow(2, size + 1))
if color_table.size() != proper_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:
var max_block_size = 254
func add_image_data_block(lzw_min_code_size: int, _data: PackedByteArray) -> void:
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 start_block_index = i * max_block_size
var end_block_index = (i * max_block_size) + max_block_size - 1
# final block can be smaller than max block size
end_block_index = (
end_block_index
if end_block_index < frame_data.size() - 1
else frame_data.size() - 1
)
var block_size = end_block_index - start_block_index + 1
var block = frame_data.subarray(start_block_index, end_block_index)
# store block size and it's data
data.append(block_size)
data.append_array(block)
if not frame_data.empty():
var block_size_index: int = 0
var i: int = 0
var data_index: int = 0
while data_index < _data.size():
if i == 0:
data.append(0)
block_size_index = data.size() - 1
data.append(_data[data_index])
data[block_size_index] += 1
data_index += 1
i += 1
if i == 254:
i = 0
if not _data.is_empty():
data.append(0)

View file

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

View file

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

View file

@ -1,38 +1,84 @@
extends Reference
extends RefCounted
var lsbbitpacker = preload("./lsbbitpacker.gd")
var lsbbitunpacker = preload("./lsbbitunpacker.gd")
var code_table := {}
var entries_counter := 0
var lsbbitpacker := preload("./lsbbitpacker.gd")
var lsbbitunpacker := preload("./lsbbitunpacker.gd")
func get_bit_length(value: int):
# bitwise or on value does ensure that the function works with value 0
# long number at the end is log(2.0)
return ceil(log(value | 0x1 + 1) / 0.6931471805599453)
class CodeEntry:
var sequence: PackedByteArray
var raw_array: Array
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:
code_table.clear()
entries_counter = 0
class CodeTable:
var entries: Dictionary = {}
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:
# warning-ignore:return_value_discarded
code_table[PoolByteArray([color_id])] = entries_counter
entries_counter += 1
result_code_table.add(CodeEntry.new([color_id]))
# move counter to the first available compression code index
var last_color_index: int = colors.size() - 1
var clear_code_index: int = pow(2, get_bit_length(last_color_index))
entries_counter = clear_code_index + 2
var clear_code_index: int = pow(2, get_bits_number_for(last_color_index))
result_code_table.counter = clear_code_index + 2
return result_code_table
# compression and decompression done with source:
# 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_color_code_table(colors)
var code_table: CodeTable = initialize_color_code_table(colors)
# Clear Code index is 2**<code size>
# <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
@ -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
# colors down.
var last_color_index: int = colors.size() - 1
var clear_code_index: int = pow(2, get_bit_length(last_color_index))
var current_code_size: int = get_bit_length(clear_code_index)
var binary_code_stream = lsbbitpacker.LSBLZWBitPacker.new()
var clear_code_index: int = pow(2, get_bits_number_for(last_color_index))
var index_stream: PackedByteArray = image
var current_code_size: int = get_bits_number_for(clear_code_index)
var binary_code_stream := lsbbitpacker.LSBLZWBitPacker.new()
# initialize with Clear Code
binary_code_stream.write_bits(clear_code_index, current_code_size)
# 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
# <LOOP POINT>
while data_index < index_stream.size():
# 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
# Is index buffer + k in our code table?
var new_index_buffer := PoolByteArray(index_buffer)
new_index_buffer.push_back(k)
var new_index_buffer := index_buffer.add(k)
if code_table.has(new_index_buffer): # if YES
# Add k to the end of the index buffer
index_buffer = new_index_buffer
else: # if NO
# 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
# index.
var last_entry_index: int = entries_counter - 1
var last_entry_index: int = code_table.counter - 1
if last_entry_index != 4095:
# Output the code for just the index buffer to our code stream
# warning-ignore:return_value_discarded
code_table[new_index_buffer] = entries_counter
entries_counter += 1
code_table.add(new_index_buffer)
else:
# if we exceeded 4095 index (code table is full), we should
# output Clear Code and reset everything.
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
# 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
# change current code size when it happens because we want to save
# 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:
current_code_size = new_code_size_candidate
# Index buffer is set to k
index_buffer = PoolByteArray([k])
index_buffer = k
# 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
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]
# 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:
return PoolByteArray([value & 255, (value >> 8) & 255])
func int_to_2bytes(value: int) -> PackedByteArray:
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
func longest_axis(colors: Array) -> int:
var start := [255, 255, 255]
var end := [0, 0, 0]
var start: PackedInt32Array = [255, 255, 255]
var end: PackedInt32Array = [0, 0, 0]
for color in colors:
for i in 3:
start[i] = min(color[i], start[i])
end[i] = max(color[i], end[i])
start[i] = mini(color[i], start[i])
end[i] = maxi(color[i], end[i])
var max_r = end[0] - start[0]
var max_g = end[1] - start[1]
var max_b = end[2] - start[2]
var max_r := end[0] - start[0]
var max_g := end[1] - start[1]
var max_b := end[2] - start[2]
if max_r > max_g:
if max_r > max_b:
@ -73,16 +73,14 @@ func average_colors(buckets: Array) -> Dictionary:
func pixels_to_colors(image: Image) -> Array:
image.lock()
var result := []
var data: PoolByteArray = image.get_data()
var data: PackedByteArray = image.get_data()
for i in range(0, data.size(), 4):
if data[i + 3] == 0:
transparency = true
continue
result.append([data[i], data[i + 1], data[i + 2]])
image.unlock()
return result
@ -93,7 +91,7 @@ func remove_smallest_bucket(buckets: Array) -> Array:
for i in range(buckets.size()):
if buckets[i].size() < buckets[i_of_smallest_bucket].size():
i_of_smallest_bucket = i
buckets.remove(i_of_smallest_bucket)
buckets.remove_at(i_of_smallest_bucket)
return buckets
@ -103,15 +101,15 @@ func remove_empty_buckets(buckets: Array) -> Array:
var i := buckets.find([])
while i != -1:
buckets.remove(i)
buckets.remove_at(i)
i = buckets.find([])
return buckets
# quantizes to gif ready codes
## Quantizes to gif ready codes
func quantize(image: Image) -> Array:
var pixels = pixels_to_colors(image)
var pixels := pixels_to_colors(image)
if pixels.size() == 0:
return pixels
@ -158,6 +156,6 @@ func quantize(image: Image) -> Array:
if transparency:
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]

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
const TRANSLATIONS_PATH := "res://addons/keychain/translations"
const PROFILES_PATH := "user://shortcut_profiles"
# Change these settings
var profiles := [preload("profiles/default.tres")]
var selected_profile: ShortcutProfile = profiles[0]
## Change these settings
var profiles: Array[ShortcutProfile] = [preload("profiles/default.tres")]
var selected_profile := profiles[0]
var profile_index := 0
# Syntax: "action_name": InputAction.new("Action Display Name", "Group", true)
# Note that "action_name" must already exist in the Project's Input Map.
## Syntax: "action_name": InputAction.new("Action Display Name", "Group", true)
## Note that "action_name" must already exist in the Project's Input Map.
var actions := {}
# Syntax: "Group Name": InputGroup.new("Parent Group Name")
## Syntax: "Group Name": InputGroup.new("Parent Group Name")
var groups := {}
var ignore_actions := []
var ignore_ui_actions := true
var changeable_types := [true, true, true, true]
var multiple_menu_accelerators := false
var config_path := "user://cache.ini"
var config_file: ConfigFile
@ -25,79 +23,11 @@ class InputAction:
var group := ""
var global := true
func _init(_display_name := "", _group := "", _global := true) -> void:
func _init(_display_name := "", _group := "", _global := true):
display_name = _display_name
group = _group
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:
var parent_group := ""
@ -112,21 +42,18 @@ class InputGroup:
func _ready() -> void:
if !config_file:
config_file = ConfigFile.new()
if !config_path.empty():
if !config_path.is_empty():
config_file.load(config_path)
set_process_input(multiple_menu_accelerators)
# Load shortcut profiles
var profile_dir := Directory.new()
profile_dir.make_dir(PROFILES_PATH)
profile_dir.open(PROFILES_PATH)
DirAccess.make_dir_recursive_absolute(PROFILES_PATH)
var profile_dir := DirAccess.open(PROFILES_PATH)
profile_dir.list_dir_begin()
var file_name = profile_dir.get_next()
while file_name != "":
if !profile_dir.current_is_dir():
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:
profiles.append(file)
file_name = profile_dir.get_next()
@ -135,7 +62,7 @@ func _ready() -> void:
if profiles.size() == 1:
var profile := ShortcutProfile.new()
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()
if saved:
profiles.append(profile)
@ -143,37 +70,9 @@ func _ready() -> void:
for profile in profiles:
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)
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:
if index >= profiles.size():
@ -184,33 +83,21 @@ func change_profile(index: int) -> void:
action_erase_events(action)
for event in selected_profile.bindings[action]:
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:
InputMap.action_add_event(action, event)
if action in actions:
actions[action].update_node(action)
func action_erase_event(action: String, event: InputEvent) -> void:
InputMap.action_erase_event(action, event)
if action in actions:
actions[action].update_node(action)
func action_erase_events(action: String) -> void:
InputMap.action_erase_events(action)
if action in actions:
actions[action].update_node(action)
func action_get_first_key(action: String) -> InputEventKey:
var first_key: InputEventKey = null
var events := InputMap.get_action_list(action)
for event in events:
if event is InputEventKey:
first_key = event
break
return first_key
func load_translation(locale: String) -> void:
var translation = load("res://addons/keychain/translations".path_join(locale + ".po"))
if is_instance_valid(translation) and translation is Translation:
TranslationServer.add_translation(translation)

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 }
const MOUSE_BUTTON_NAMES := [
const MOUSE_BUTTON_NAMES: PackedStringArray = [
"Left Button",
"Right Button",
"Middle Button",
@ -14,7 +14,7 @@ const MOUSE_BUTTON_NAMES := [
"X Button 2",
]
const JOY_BUTTON_NAMES := [
const JOY_BUTTON_NAMES: PackedStringArray = [
"DualShock Cross, Xbox A, Nintendo B",
"DualShock Circle, Xbox B, Nintendo A",
"DualShock Square, Xbox X, Nintendo Y",
@ -40,7 +40,7 @@ const JOY_BUTTON_NAMES := [
"PS4/5 Touchpad",
]
const JOY_AXIS_NAMES := [
const JOY_AXIS_NAMES: PackedStringArray = [
"(Left Stick Left)",
"(Left Stick Right)",
"(Left Stick Up)",
@ -66,29 +66,29 @@ const JOY_AXIS_NAMES := [
var currently_editing_tree_item: TreeItem
var is_editing := false
# Textures taken from Godot https://github.com/godotengine/godot/tree/master/editor/icons
var add_tex: Texture = preload("assets/add.svg")
var edit_tex: Texture = preload("assets/edit.svg")
var delete_tex: Texture = preload("assets/close.svg")
var joy_axis_tex: Texture = preload("assets/joy_axis.svg")
var joy_button_tex: Texture = preload("assets/joy_button.svg")
var key_tex: Texture = preload("assets/keyboard.svg")
var key_phys_tex: Texture = preload("assets/keyboard_physical.svg")
var mouse_tex: Texture = preload("assets/mouse.svg")
var shortcut_tex: Texture = preload("assets/shortcut.svg")
var folder_tex: Texture = preload("assets/folder.svg")
var add_tex: Texture2D = preload("assets/add.svg")
var edit_tex: Texture2D = preload("assets/edit.svg")
var delete_tex: Texture2D = preload("assets/close.svg")
var joy_axis_tex: Texture2D = preload("assets/joy_axis.svg")
var joy_button_tex: Texture2D = preload("assets/joy_button.svg")
var key_tex: Texture2D = preload("assets/keyboard.svg")
var key_phys_tex: Texture2D = preload("assets/keyboard_physical.svg")
var mouse_tex: Texture2D = preload("assets/mouse.svg")
var shortcut_tex: Texture2D = preload("assets/shortcut.svg")
var folder_tex: Texture2D = preload("assets/folder.svg")
onready var tree: Tree = $VBoxContainer/ShortcutTree
onready var profile_option_button: OptionButton = find_node("ProfileOptionButton")
onready var rename_profile_button: Button = find_node("RenameProfile")
onready var delete_profile_button: Button = find_node("DeleteProfile")
onready var shortcut_type_menu: PopupMenu = $ShortcutTypeMenu
onready var keyboard_shortcut_selector: ConfirmationDialog = $KeyboardShortcutSelectorDialog
onready var mouse_shortcut_selector: ConfirmationDialog = $MouseShortcutSelectorDialog
onready var joy_key_shortcut_selector: ConfirmationDialog = $JoyKeyShortcutSelectorDialog
onready var joy_axis_shortcut_selector: ConfirmationDialog = $JoyAxisShortcutSelectorDialog
onready var profile_settings: ConfirmationDialog = $ProfileSettings
onready var profile_name: LineEdit = $ProfileSettings/ProfileName
onready var delete_confirmation: ConfirmationDialog = $DeleteConfirmation
@onready var tree: Tree = $VBoxContainer/ShortcutTree
@onready var profile_option_button: OptionButton = find_child("ProfileOptionButton")
@onready var rename_profile_button: Button = find_child("RenameProfile")
@onready var delete_profile_button: Button = find_child("DeleteProfile")
@onready var shortcut_type_menu: PopupMenu = $ShortcutTypeMenu
@onready var keyboard_shortcut_selector: ConfirmationDialog = $KeyboardShortcutSelectorDialog
@onready var mouse_shortcut_selector: ConfirmationDialog = $MouseShortcutSelectorDialog
@onready var joy_key_shortcut_selector: ConfirmationDialog = $JoyKeyShortcutSelectorDialog
@onready var joy_axis_shortcut_selector: ConfirmationDialog = $JoyAxisShortcutSelectorDialog
@onready var profile_settings: ConfirmationDialog = $ProfileSettings
@onready var profile_name: LineEdit = $ProfileSettings/ProfileName
@onready var delete_confirmation: ConfirmationDialog = $DeleteConfirmation
func _ready() -> void:
@ -107,7 +107,7 @@ func _ready() -> void:
profile_option_button.select(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()
@ -121,7 +121,7 @@ func _construct_tree() -> void:
for action in InputMap.get_actions(): # Fill the tree with actions and their events
if action in Keychain.ignore_actions:
continue
if Keychain.ignore_ui_actions and action.begins_with("ui_"):
if Keychain.ignore_ui_actions and (action as String).begins_with("ui_"):
continue
var display_name := get_action_name(action)
@ -131,7 +131,7 @@ func _construct_tree() -> void:
group_name = input_action.group
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 group_root: TreeItem = input_group.tree_item
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_metadata(0, action)
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)
tree_item.add_button(0, add_tex, 0, buttons_disabled, "Add")
@ -151,8 +151,6 @@ func _construct_tree() -> 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:"
joy_key_shortcut_selector.input_type_l.text = "Joypad Button 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 i := 0.0
for option in JOY_AXIS_NAMES:
var sign_symbol = "+" if floor(i) != i else "-"
var text: String = tr("Axis") + " %s %s %s" % [floor(i), sign_symbol, tr(option)]
var sign_symbol := "+" if floori(i) != i else "-"
var text: String = tr("Axis") + " %s %s %s" % [floori(i), sign_symbol, tr(option)]
joy_axis_option_button.add_item(text)
i += 0.5
@ -200,7 +198,7 @@ func get_action_name(action: String) -> String:
if action in Keychain.actions:
display_name = Keychain.actions[action].display_name
if display_name.empty():
if display_name.is_empty():
display_name = _humanize_snake_case(action)
return display_name
@ -209,7 +207,7 @@ func _humanize_snake_case(text: String) -> String:
text = text.replace("_", " ")
var first_letter := text.left(1)
first_letter = first_letter.capitalize()
text.erase(0, 1)
text = text.right(-1)
text = text.insert(0, first_letter)
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)
match event_class:
"InputEventKey":
var scancode: int = event.get_scancode_with_modifiers()
var scancode: int = event.get_keycode_with_modifiers()
if scancode > 0:
event_tree_item.set_icon(0, key_tex)
else:
@ -252,19 +250,10 @@ func add_event_tree_item(event: InputEvent, action_tree_item: TreeItem) -> void:
func event_to_str(event: InputEvent) -> String:
var output := ""
if event is InputEventKey:
var scancode: int = event.get_scancode_with_modifiers()
var physical_str := ""
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 output := event.as_text()
# event.as_text() could be used for these event types as well, but this gives more control
# to the developer as to what strings will be printed
if event is InputEventJoypadButton:
var button_index: int = event.button_index
output = tr("Button")
if button_index >= JOY_BUTTON_NAMES.size():
@ -281,24 +270,22 @@ func event_to_str(event: InputEvent) -> String:
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)
currently_editing_tree_item = item
if action is String:
if action is StringName:
if id == 0: # Add
var rect: Rect2 = tree.get_item_area_rect(item, 0)
rect.position.x = rect.end.x - 42
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())
shortcut_type_menu.popup(rect)
elif id == 1: # Delete
Keychain.action_erase_events(action)
Keychain.selected_profile.change_action(action)
var child := item.get_children()
while child != null:
for child in item.get_children():
child.free()
child = item.get_children()
elif action is InputEvent:
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:
joy_axis_shortcut_selector.popup_centered()
elif id == 1: # Delete
if not parent_action is String:
if not parent_action is StringName:
return
Keychain.action_erase_event(parent_action, 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:
var selected_item: TreeItem = tree.get_selected()
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:
@ -353,14 +340,14 @@ func _on_ProfileOptionButton_item_selected(index: int) -> void:
func _on_NewProfile_pressed() -> void:
is_editing = false
profile_name.text = "New Shortcut Profile"
profile_settings.window_title = "New Shortcut Profile"
profile_settings.title = "New Shortcut Profile"
profile_settings.popup_centered()
func _on_RenameProfile_pressed() -> void:
is_editing = true
profile_name.text = Keychain.selected_profile.name
profile_settings.window_title = "Rename Shortcut Profile"
profile_settings.title = "Rename Shortcut Profile"
profile_settings.popup_centered()
@ -376,7 +363,7 @@ func _on_ProfileSettings_confirmed() -> void:
var file_name := profile_name.text + ".tres"
var profile := ShortcutProfile.new()
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()
var saved := profile.save()
if not saved:
@ -397,14 +384,18 @@ func _on_ProfileSettings_confirmed() -> 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)
func _on_DeleteConfirmation_confirmed() -> void:
_delete_profile_file(Keychain.selected_profile.resource_path)
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
if 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 path="res://addons/keychain/assets/joy_button.svg" type="Texture" id=2]
[ext_resource path="res://addons/keychain/assets/keyboard.svg" type="Texture" id=3]
[ext_resource path="res://addons/keychain/assets/joy_axis.svg" type="Texture" id=4]
[ext_resource path="res://addons/keychain/assets/mouse.svg" type="Texture" id=5]
[ext_resource path="res://addons/keychain/ShortcutSelectorDialog.tscn" type="PackedScene" id=6]
[ext_resource type="Script" path="res://addons/keychain/ShortcutEdit.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://ca58ufal2ufd8" path="res://addons/keychain/assets/joy_button.svg" id="2"]
[ext_resource type="Texture2D" uid="uid://c2s5rm4nec5yh" path="res://addons/keychain/assets/keyboard.svg" id="3"]
[ext_resource type="Texture2D" uid="uid://bb6q6om3d08cm" path="res://addons/keychain/assets/joy_axis.svg" id="4"]
[ext_resource type="Texture2D" uid="uid://bma7xj2rqqcr8" path="res://addons/keychain/assets/mouse.svg" id="5"]
[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_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
script = ExtResource( 1 )
script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
margin_right = 1280.0
margin_bottom = 720.0
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
margin_right = 1280.0
margin_bottom = 20.0
layout_mode = 2
[node name="ProfileLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
margin_top = 3.0
margin_right = 102.0
margin_bottom = 17.0
layout_mode = 2
text = "Shortcut profile:"
[node name="ProfileOptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"]
margin_left = 106.0
margin_right = 135.0
margin_bottom = 20.0
layout_mode = 2
mouse_default_cursor_shape = 2
[node name="NewProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 139.0
margin_right = 179.0
margin_bottom = 20.0
layout_mode = 2
mouse_default_cursor_shape = 2
text = "New"
[node name="RenameProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 183.0
margin_right = 247.0
margin_bottom = 20.0
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Rename"
[node name="DeleteProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 251.0
margin_right = 306.0
margin_bottom = 20.0
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Delete"
[node name="OpenProfileFolder" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 310.0
margin_right = 401.0
margin_bottom = 20.0
layout_mode = 2
mouse_default_cursor_shape = 2
text = "Open Folder"
[node name="ShortcutTree" type="Tree" parent="VBoxContainer"]
margin_top = 24.0
margin_right = 1280.0
margin_bottom = 720.0
layout_mode = 2
size_flags_vertical = 3
hide_root = true
[node name="ShortcutTypeMenu" type="PopupMenu" parent="."]
margin_right = 20.0
margin_bottom = 20.0
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 ]
size = Vector2i(154, 116)
item_count = 4
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
[node name="JoyKeyShortcutSelectorDialog" parent="." instance=ExtResource( 6 )]
[node name="JoyKeyShortcutSelectorDialog" parent="." instance=ExtResource("6")]
size = Vector2i(417, 134)
input_type = 2
[node name="JoyAxisShortcutSelectorDialog" parent="." instance=ExtResource( 6 )]
[node name="JoyAxisShortcutSelectorDialog" parent="." instance=ExtResource("6")]
size = Vector2i(417, 134)
input_type = 3
[node name="ProfileSettings" type="ConfirmationDialog" parent="."]
margin_right = 200.0
margin_bottom = 70.0
[node name="ProfileName" type="LineEdit" parent="ProfileSettings"]
margin_left = 8.0
margin_top = 8.0
margin_right = 192.0
margin_bottom = 34.0
caret_blink = true
caret_blink_speed = 0.5
offset_left = 8.0
offset_top = 8.0
offset_right = 192.0
offset_bottom = 51.0
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
margin_right = 200.0
margin_bottom = 70.0
size = Vector2i(427, 100)
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"]
@ -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/DeleteProfile" to="." method="_on_DeleteProfile_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="id_pressed" from="ShortcutTypeMenu" to="." method="_on_ShortcutTypeMenu_id_pressed"]
[connection signal="confirmed" from="ProfileSettings" to="." method="_on_ProfileSettings_confirmed"]

View file

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

View file

@ -2,28 +2,39 @@ extends ConfirmationDialog
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
onready var root: Node = get_parent()
onready var input_type_l: Label = $VBoxContainer/InputTypeLabel
onready var entered_shortcut: LineEdit = $VBoxContainer/EnteredShortcut
onready var option_button: OptionButton = $VBoxContainer/OptionButton
onready var already_exists: Label = $VBoxContainer/AlreadyExistsLabel
@onready var root := get_parent()
@onready var input_type_l := $VBoxContainer/InputTypeLabel as Label
@onready var entered_shortcut := $VBoxContainer/EnteredShortcut as LineEdit
@onready var option_button := $VBoxContainer/OptionButton as OptionButton
@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:
set_process_input(false)
if input_type == InputTypes.KEYBOARD:
get_ok().focus_neighbour_top = entered_shortcut.get_path()
get_cancel().focus_neighbour_top = entered_shortcut.get_path()
entered_shortcut.focus_neighbour_bottom = get_ok().get_path()
entered_shortcut.visible = true
option_button.visible = false
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:
get_ok().focus_neighbour_top = option_button.get_path()
get_cancel().focus_neighbour_top = option_button.get_path()
option_button.focus_neighbour_bottom = get_ok().get_path()
if input_type != InputTypes.MOUSE:
modifier_buttons.visible = false
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:
@ -31,7 +42,8 @@ func _input(event: InputEvent) -> void:
return
if event.pressed:
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)
@ -40,7 +52,7 @@ func _show_assigned_state(event: InputEvent) -> void:
var action := ""
if metadata is InputEvent: # Editing an input event
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
var matching_pair: Array = _find_matching_event_in_map(action, event)
@ -65,14 +77,14 @@ func _apply_shortcut_change(input_event: InputEvent) -> void:
return
root.currently_editing_tree_item.set_metadata(0, 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)
if !changed:
return
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
return false
if old_event:
@ -86,7 +98,7 @@ func _set_shortcut(action: String, old_event: InputEvent, new_event: InputEvent)
if action in Keychain.actions:
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]
Keychain.action_erase_event(action_to_replace, input_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...
var metadata = tree_item.get_metadata(0)
if metadata is InputEvent:
if input_to_replace.shortcut_match(metadata):
var map_action: String = tree_item.get_parent().get_metadata(0)
if input_to_replace.is_match(metadata):
var map_action: StringName = tree_item.get_parent().get_metadata(0)
if map_action == action_to_replace:
tree_item.free()
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.selected_profile.change_action(action)
return true
# Based on https://github.com/godotengine/godot/blob/master/scene/gui/tree.cpp#L685
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:
func _find_matching_event_in_map(action: StringName, event: InputEvent) -> Array:
var group := ""
if action in Keychain.actions:
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():
if map_action in Keychain.ignore_actions:
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
for map_event in InputMap.get_action_list(map_action):
if !event.shortcut_match(map_event):
for map_event in InputMap.action_get_events(map_action):
if !event.is_match(map_event):
continue
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:
var metadata = root.currently_editing_tree_item.get_metadata(0)
if input_type == InputTypes.KEYBOARD:
listened_input = null
already_exists.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()
else:
var metadata = root.currently_editing_tree_item.get_metadata(0)
if metadata is InputEvent: # Editing an input event
var index := 0
if metadata is InputEventMouseButton:
index = metadata.button_index - 1
_set_modifier_buttons_state(metadata)
elif metadata is InputEventJoypadButton:
index = metadata.button_index
elif metadata is InputEventJoypadMotion:
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)
_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)
_on_OptionButton_item_selected(0)
@ -181,6 +182,11 @@ func _on_OptionButton_item_selected(index: int) -> void:
if input_type == InputTypes.MOUSE:
listened_input = InputEventMouseButton.new()
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:
listened_input = InputEventJoypadButton.new()
listened_input.button_index = index
@ -197,3 +203,50 @@ func _on_EnteredShortcut_focus_entered() -> void:
func _on_EnteredShortcut_focus_exited() -> void:
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"]
margin_right = 200.0
margin_bottom = 70.0
popup_exclusive = true
window_title = "Set the shortcut"
script = ExtResource( 1 )
size = Vector2i(417, 169)
script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
margin_left = 8.0
margin_top = 8.0
margin_right = 341.0
margin_bottom = 64.0
offset_left = 8.0
offset_top = 8.0
offset_right = 409.0
offset_bottom = 120.0
[node name="InputTypeLabel" type="Label" parent="VBoxContainer"]
margin_right = 333.0
margin_bottom = 14.0
layout_mode = 2
text = "Press a key or a key combination to set the shortcut"
[node name="EnteredShortcut" type="LineEdit" parent="VBoxContainer"]
visible = false
margin_top = 18.0
margin_right = 333.0
margin_bottom = 32.0
align = 1
layout_mode = 2
editable = false
virtual_keyboard_enabled = false
[node name="OptionButton" type="OptionButton" parent="VBoxContainer"]
margin_top = 18.0
margin_right = 333.0
margin_bottom = 38.0
layout_mode = 2
mouse_default_cursor_shape = 2
[node name="AlreadyExistsLabel" type="Label" parent="VBoxContainer"]
margin_top = 42.0
margin_right = 333.0
margin_bottom = 56.0
align = 1
[node name="ModifierButtons" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[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="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_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="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]
importer="texture"
type="StreamTexture"
path="res://.import/add.svg-4084e314648c872072757f9b0f544cf9.stex"
type="CompressedTexture2D"
uid="uid://c4433pa25cxp2"
path="res://.godot/imported/add.svg-4084e314648c872072757f9b0f544cf9.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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 +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]
importer="texture"
type="StreamTexture"
path="res://.import/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.stex"
type="CompressedTexture2D"
uid="uid://f1gcylay6c6f"
path="res://.godot/imported/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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 +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]
importer="texture"
type="StreamTexture"
path="res://.import/edit.svg-cd9834545a8696f1e8611efa12a48f33.stex"
type="CompressedTexture2D"
uid="uid://df8gl3u2nqpl3"
path="res://.godot/imported/edit.svg-cd9834545a8696f1e8611efa12a48f33.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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 +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]
importer="texture"
type="StreamTexture"
path="res://.import/folder.svg-490bb7e2d2aa4425de998b1d382cf534.stex"
type="CompressedTexture2D"
uid="uid://b0gbmkb8xwksb"
path="res://.godot/imported/folder.svg-490bb7e2d2aa4425de998b1d382cf534.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.stex"
type="CompressedTexture2D"
uid="uid://bb6q6om3d08cm"
path="res://.godot/imported/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/joy_button.svg-df5663c6f296cab556e81b9c770699d0.stex"
type="CompressedTexture2D"
uid="uid://ca58ufal2ufd8"
path="res://.godot/imported/joy_button.svg-df5663c6f296cab556e81b9c770699d0.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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 +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]
importer="texture"
type="StreamTexture"
path="res://.import/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.stex"
type="CompressedTexture2D"
uid="uid://c2s5rm4nec5yh"
path="res://.godot/imported/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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 +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]
importer="texture"
type="StreamTexture"
path="res://.import/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.stex"
type="CompressedTexture2D"
uid="uid://cmh8eaibhn5y8"
path="res://.godot/imported/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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 +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]
importer="texture"
type="StreamTexture"
path="res://.import/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.stex"
type="CompressedTexture2D"
uid="uid://bma7xj2rqqcr8"
path="res://.godot/imported/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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 +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]
importer="texture"
type="StreamTexture"
path="res://.import/shortcut.svg-401daac18a817142e2b29e5801e00053.stex"
type="CompressedTexture2D"
uid="uid://bmi24jp4fqi7k"
path="res://.godot/imported/shortcut.svg-401daac18a817142e2b29e5801e00053.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
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

@ -3,5 +3,5 @@
name="Keychain"
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"
version="1.1"
version="2.0"
script="plugin.gd"

View file

@ -1,4 +1,4 @@
tool
@tool
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]
script = ExtResource( 1 )
script = ExtResource("1")
name = "Default"
customizable = false
bindings = {
}
bindings = {}

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/bayer16.png-48f80e5e8f0bdac9e83b3acfa8588cf2.stex"
type="CompressedTexture2D"
uid="uid://cdtpmo4pjjjdd"
path="res://.godot/imported/bayer16.png-48f80e5e8f0bdac9e83b3acfa8588cf2.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=1
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/bayer2.png-c060ec2442edc204b9ec0ab2d99e19bf.stex"
type="CompressedTexture2D"
uid="uid://cdoxrolk7qdju"
path="res://.godot/imported/bayer2.png-c060ec2442edc204b9ec0ab2d99e19bf.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=1
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/bayer4.png-2008f73a4dd6b2f9f4ba6aac825a7b9b.stex"
type="CompressedTexture2D"
uid="uid://oald6mp1l1h7"
path="res://.godot/imported/bayer4.png-2008f73a4dd6b2f9f4ba6aac825a7b9b.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=1
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/bayer8.png-2c0f9ff189b3d219f822ce84a94f5abd.stex"
type="CompressedTexture2D"
uid="uid://bq45j7jxav8nw"
path="res://.godot/imported/bayer8.png-2c0f9ff189b3d219f822ce84a94f5abd.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=1
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=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"
type="Image"
path="res://.import/circle_9x9.png-775114b180b1f2b8d06dc42243fc410b.image"
uid="uid://bccayivs7v851"
path="res://.godot/imported/circle_9x9.png-775114b180b1f2b8d06dc42243fc410b.image"
[deps]
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]

View file

@ -2,12 +2,13 @@
importer="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]
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]

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/cursor.png-45ecd5a1e583fe3ca68fe559623aadce.stex"
type="CompressedTexture2D"
uid="uid://busi7lojhqxgq"
path="res://.godot/imported/cursor.png-45ecd5a1e583fe3ca68fe559623aadce.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/dotted_line.png-9a94e4e6ebf9c11e53076f53e529d37a.stex"
type="CompressedTexture2D"
uid="uid://xnqvppcpt7jb"
path="res://.godot/imported/dotted_line.png-9a94e4e6ebf9c11e53076f53e529d37a.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=1
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/directional_light.svg-093cdb9a72dee590271da014181a4680.stex"
type="CompressedTexture2D"
uid="uid://bd25ysikhyhqn"
path="res://.godot/imported/directional_light.svg-093cdb9a72dee590271da014181a4680.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=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,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.stex"
type="CompressedTexture2D"
uid="uid://cu38hb880ltae"
path="res://.godot/imported/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=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,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.stex"
type="CompressedTexture2D"
uid="uid://ded88bscdjle8"
path="res://.godot/imported/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,27 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=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,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/icon.png-aa34c86f977c2e615fbfe8e823c3b00c.stex"
type="CompressedTexture2D"
uid="uid://b47r0c6auaqk6"
path="res://.godot/imported/icon.png-aa34c86f977c2e615fbfe8e823c3b00c.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/clone.png-04ad708db9ea2b00606e50fe99f4ca29.stex"
type="CompressedTexture2D"
uid="uid://d3gx4phcox58s"
path="res://.godot/imported/clone.png-04ad708db9ea2b00606e50fe99f4ca29.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/delete.png-78cc7779425d13dd019b2b6b51aca5b2.stex"
type="CompressedTexture2D"
uid="uid://x2k652y15v04"
path="res://.godot/imported/delete.png-78cc7779425d13dd019b2b6b51aca5b2.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/group_collapsed.png-9d08fac1c2f635c754860111d024aa0f.stex"
type="CompressedTexture2D"
uid="uid://d11kh3e3nq1l5"
path="res://.godot/imported/group_collapsed.png-9d08fac1c2f635c754860111d024aa0f.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/group_expanded.png-f3cd620185a4989737d6d9a36f4b57f3.stex"
type="CompressedTexture2D"
uid="uid://dndlglvqc7v6a"
path="res://.godot/imported/group_expanded.png-f3cd620185a4989737d6d9a36f4b57f3.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/group_new.png-4ebdc7dd84d8c8a7b7979f50f4471543.stex"
type="CompressedTexture2D"
uid="uid://cmgnc2emsbxni"
path="res://.godot/imported/group_new.png-4ebdc7dd84d8c8a7b7979f50f4471543.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/layer_invisible.png-b8a35d26d7a70c1d9e228e24fcffb0d2.stex"
type="CompressedTexture2D"
uid="uid://dwqdm6pllnipj"
path="res://.godot/imported/layer_invisible.png-b8a35d26d7a70c1d9e228e24fcffb0d2.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -1,8 +1,9 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/layer_visible.png-28b5ac4566a2e156d8e7d4caecc5f889.stex"
type="CompressedTexture2D"
uid="uid://c2b3htff5yox8"
path="res://.godot/imported/layer_visible.png-28b5ac4566a2e156d8e7d4caecc5f889.ctex"
metadata={
"vram_texture": false
}
@ -10,26 +11,24 @@ metadata={
[deps]
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]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/hdr_compression=1
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
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/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

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