mirror of https://github.com/aseprite/aseprite.git
Compare commits
133 Commits
8c2b06532d
...
f6b23dc4e8
| Author | SHA1 | Date |
|---|---|---|
|
|
f6b23dc4e8 | |
|
|
9fd1214947 | |
|
|
286dd1c755 | |
|
|
3a14ac72a4 | |
|
|
2db193b8e3 | |
|
|
002356ce19 | |
|
|
9a1e92da35 | |
|
|
07803ff361 | |
|
|
8e07617a9d | |
|
|
9c5ca6bcc6 | |
|
|
e193891df3 | |
|
|
bb8547d004 | |
|
|
85997a08cf | |
|
|
0995e72a6f | |
|
|
f61c2c3950 | |
|
|
0c49f2d7ad | |
|
|
5f7cc42333 | |
|
|
8e75cfc4c7 | |
|
|
983b07383f | |
|
|
c57554646b | |
|
|
74953174d6 | |
|
|
49fa35237a | |
|
|
0ccf9dcc4f | |
|
|
194f8424a8 | |
|
|
debab653fa | |
|
|
6e9024d54d | |
|
|
1fa7fd0831 | |
|
|
ab6b040e83 | |
|
|
bc312a37b3 | |
|
|
3129fda977 | |
|
|
6cb61fb41e | |
|
|
aa817a8d2a | |
|
|
40031f83d8 | |
|
|
90282dbc40 | |
|
|
1227f9c49c | |
|
|
d61ae919ad | |
|
|
b2b2583176 | |
|
|
b535212642 | |
|
|
229a3cdf65 | |
|
|
b3814ec912 | |
|
|
e88f3bb413 | |
|
|
eaa2bdf0af | |
|
|
57309e5aa5 | |
|
|
4bb9239f50 | |
|
|
cef92c1a38 | |
|
|
22e72ab5cb | |
|
|
80fa065bd5 | |
|
|
de1ccb24dd | |
|
|
7d91c4b9d9 | |
|
|
6d89a6bc15 | |
|
|
d4e97b5a96 | |
|
|
205b18dc0f | |
|
|
2ba051b59b | |
|
|
3fcb000eb1 | |
|
|
af9dc3c817 | |
|
|
250dfdc86a | |
|
|
c904c41b39 | |
|
|
9e941e9a8b | |
|
|
bbab4d5875 | |
|
|
5c4daff128 | |
|
|
11a7b061ff | |
|
|
283bedf77e | |
|
|
2eeb6f04a7 | |
|
|
706d0b8a7a | |
|
|
2f3a7f5dec | |
|
|
d5de74b715 | |
|
|
2d87a7b184 | |
|
|
2f22804fe8 | |
|
|
bf1b4c6f50 | |
|
|
250244c777 | |
|
|
b4555fc098 | |
|
|
8783135bf7 | |
|
|
1a6a39700e | |
|
|
ce742bcbc1 | |
|
|
3c350c3e67 | |
|
|
220d2d3a2d | |
|
|
322040c4fb | |
|
|
ee69a2f2f6 | |
|
|
91f8410749 | |
|
|
8d5534d4eb | |
|
|
2ee8c68d94 | |
|
|
f7040190cc | |
|
|
68bf0839aa | |
|
|
e5917389cb | |
|
|
27eecb3bdc | |
|
|
b6cbeefb6e | |
|
|
5596c3270d | |
|
|
ab29b84f25 | |
|
|
444ef0f6b4 | |
|
|
b3b956516b | |
|
|
32bf699655 | |
|
|
5ceeac2f26 | |
|
|
0ddf7d939b | |
|
|
bd13e5d574 | |
|
|
e53aa99080 | |
|
|
af6e8b65c3 | |
|
|
68342bdb66 | |
|
|
8ff208d8d5 | |
|
|
2bc4f0582d | |
|
|
2be4dc1474 | |
|
|
2b522e222b | |
|
|
698d79b049 | |
|
|
03422e7251 | |
|
|
3f1f131a39 | |
|
|
54ea61fe58 | |
|
|
4c31c950c5 | |
|
|
afbd28b3b4 | |
|
|
1519589184 | |
|
|
2c917e30c0 | |
|
|
5d0214a89d | |
|
|
4b1d49f5dc | |
|
|
0ccc800819 | |
|
|
14c0baa3db | |
|
|
30dcac99c6 | |
|
|
d209971f07 | |
|
|
0a88b86c99 | |
|
|
ab449a2978 | |
|
|
b32cd0ff47 | |
|
|
ce0b9a6405 | |
|
|
67656c4977 | |
|
|
7817e7b37a | |
|
|
45fbeda95b | |
|
|
a3236bc1e9 | |
|
|
d37e0df18f | |
|
|
7103829e60 | |
|
|
eab8df6854 | |
|
|
99a407f0c5 | |
|
|
66123e9d57 | |
|
|
f1b6dd8594 | |
|
|
79de4da82a | |
|
|
9c3f985ee5 | |
|
|
2cb42b343f | |
|
|
38b5f3b283 |
|
|
@ -60,6 +60,8 @@ Checks: >
|
||||||
-readability-uppercase-literal-suffix
|
-readability-uppercase-literal-suffix
|
||||||
WarningsAsErrors: ''
|
WarningsAsErrors: ''
|
||||||
CheckOptions:
|
CheckOptions:
|
||||||
|
- key: readability-implicit-bool-conversion.AllowIntegerConditions
|
||||||
|
value: true
|
||||||
- key: readability-implicit-bool-conversion.AllowPointerConditions
|
- key: readability-implicit-bool-conversion.AllowPointerConditions
|
||||||
value: true
|
value: true
|
||||||
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
|
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
Describe your bug report or feature request here
|
|
||||||
...
|
|
||||||
...
|
|
||||||
...
|
|
||||||
|
|
||||||
### Aseprite and System version
|
|
||||||
|
|
||||||
* Aseprite version: version number, installer/portable/Steam/beta/dev/commit-hash
|
|
||||||
* System: Windows/macOS/Linux, version, distribution
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug, triage
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots or a screen recording to help explain your problem.
|
||||||
|
|
||||||
|
**Aseprite & System (please complete the following information):**
|
||||||
|
- Aseprite: [version number, installer/portable/Steam/beta/dev/commit-hash]
|
||||||
|
- System: [Windows/macOS/Linux, version, distribution]
|
||||||
|
- Extensions: [List the extensions you have installed]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for Aseprite
|
||||||
|
title: ''
|
||||||
|
labels: feature, triage
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Did other user suggested a similar idea?**
|
||||||
|
- [ ] No
|
||||||
|
- [ ] Yes/Links to similar ideas
|
||||||
|
> You can try to find a similar feature requests before in:
|
||||||
|
> - GitHub issues: https://github.com/aseprite/aseprite/issues?q=label%3Afeature
|
||||||
|
> - Community site: https://community.aseprite.org/c/features/7
|
||||||
|
> - Steam community: https://steamcommunity.com/app/431730/discussions/1/
|
||||||
|
> In case you find a similar feature request, making a comment there will be useful to give some traction and show interest in the feature.
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
|
|
@ -13,6 +13,6 @@ jobs:
|
||||||
post-comments:
|
post-comments:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: ZedThree/clang-tidy-review/post@v0.20.1
|
- uses: ZedThree/clang-tidy-review/post@v0.21.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CLANG_TIDY_TOKEN }}
|
token: ${{ secrets.CLANG_TIDY_TOKEN }}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ Aseprite is being developed and maintained currently by [Igara Studio](https://i
|
||||||
The active team of developers is:
|
The active team of developers is:
|
||||||
|
|
||||||
* [David Capello](https://github.com/dacap)
|
* [David Capello](https://github.com/dacap)
|
||||||
* [Gaspar Capello](https://github.com/Gsparoken)
|
* [Gaspar Capello](https://github.com/Gasparoken)
|
||||||
* [Martín Capello](https://github.com/martincapello)
|
* [Martín Capello](https://github.com/martincapello)
|
||||||
* [Christian Kaiser](https://github.com/ckaiser)
|
* [Christian Kaiser](https://github.com/ckaiser)
|
||||||
* [Dante Paola](https://github.com/Liebranca)
|
* [Dante Paola](https://github.com/Liebranca)
|
||||||
|
|
|
||||||
20
build.sh
20
build.sh
|
|
@ -160,7 +160,7 @@ if [ ! -f "$pwd/.build/userkind" ] ; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
userkind=$(echo -n $(cat $pwd/.build/userkind))
|
userkind=$(cat $pwd/.build/userkind)
|
||||||
if [ "$userkind" == "developer" ] ; then
|
if [ "$userkind" == "developer" ] ; then
|
||||||
echo "======================= BUILDING FOR DEVELOPER ======================="
|
echo "======================= BUILDING FOR DEVELOPER ======================="
|
||||||
else
|
else
|
||||||
|
|
@ -221,7 +221,7 @@ if [ ! -f "$pwd/.build/builds_dir" ] ; then
|
||||||
echo "$builds_dir" > "$pwd/.build/builds_dir"
|
echo "$builds_dir" > "$pwd/.build/builds_dir"
|
||||||
fi
|
fi
|
||||||
# Overwrite $builds_dir variable from the config content.
|
# Overwrite $builds_dir variable from the config content.
|
||||||
builds_dir="$(echo -n $(cat $pwd/.build/builds_dir))"
|
builds_dir="$(cat $pwd/.build/builds_dir)"
|
||||||
|
|
||||||
# List all builds.
|
# List all builds.
|
||||||
builds_list="$(mktemp)"
|
builds_list="$(mktemp)"
|
||||||
|
|
@ -356,7 +356,7 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Required Skia for the base branch.
|
# Required Skia for the base branch.
|
||||||
skia_tag=$(cat "$pwd/laf/misc/skia-tag.txt" | xargs)
|
skia_tag=$(cat "$pwd/laf/misc/skia-tag.txt")
|
||||||
possible_skia_dir_name=skia-$(echo $skia_tag | cut -d "-" -f 1)
|
possible_skia_dir_name=skia-$(echo $skia_tag | cut -d "-" -f 1)
|
||||||
file_skia_dir="$base_branch_name"_skia_dir
|
file_skia_dir="$base_branch_name"_skia_dir
|
||||||
|
|
||||||
|
|
@ -395,7 +395,7 @@ if [ ! -f "$pwd/.build/$file_skia_dir" ] ; then
|
||||||
fi
|
fi
|
||||||
echo $skia_dir > "$pwd/.build/$file_skia_dir"
|
echo $skia_dir > "$pwd/.build/$file_skia_dir"
|
||||||
fi
|
fi
|
||||||
skia_dir=$(echo -n $(cat $pwd/.build/$file_skia_dir))
|
skia_dir=$(cat $pwd/.build/$file_skia_dir)
|
||||||
if [ ! -d "$skia_dir" ] ; then
|
if [ ! -d "$skia_dir" ] ; then
|
||||||
mkdir "$skia_dir"
|
mkdir "$skia_dir"
|
||||||
fi
|
fi
|
||||||
|
|
@ -417,7 +417,7 @@ if [ ! -d "$skia_library_dir" ] ; then
|
||||||
if [ ! $auto ] ; then
|
if [ ! $auto ] ; then
|
||||||
read -p "Download pre-compiled Skia automatically [Y/n]? "
|
read -p "Download pre-compiled Skia automatically [Y/n]? "
|
||||||
# Convert the Enter key as the default option: an empty string
|
# Convert the Enter key as the default option: an empty string
|
||||||
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]' | xargs)
|
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
|
||||||
fi
|
fi
|
||||||
if [[ $auto || "$REPLY" == "" || "$REPLY" == "y" || "$REPLY" == "yes" ]] ; then
|
if [[ $auto || "$REPLY" == "" || "$REPLY" == "y" || "$REPLY" == "yes" ]] ; then
|
||||||
if [[ $is_win && "$build_type" == "Debug" ]] ; then
|
if [[ $is_win && "$build_type" == "Debug" ]] ; then
|
||||||
|
|
@ -425,12 +425,20 @@ if [ ! -d "$skia_library_dir" ] ; then
|
||||||
else
|
else
|
||||||
skia_build=Release
|
skia_build=Release
|
||||||
fi
|
fi
|
||||||
skia_url=$(bash laf/misc/skia-url.sh $skia_build | xargs)
|
skia_url=$(bash laf/misc/skia-url.sh $skia_build)
|
||||||
skia_file=$(basename $skia_url)
|
skia_file=$(basename $skia_url)
|
||||||
if [ ! -f "$skia_dir/$skia_file" ] ; then
|
if [ ! -f "$skia_dir/$skia_file" ] ; then
|
||||||
|
if ! command -v curl >/dev/null 2>&1 ; then
|
||||||
|
echo "Error: 'curl' command line tool is not available in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
curl --ssl-revoke-best-effort -L -o "$skia_dir/$skia_file" "$skia_url"
|
curl --ssl-revoke-best-effort -L -o "$skia_dir/$skia_file" "$skia_url"
|
||||||
fi
|
fi
|
||||||
if [ ! -d "$skia_library_dir" ] ; then
|
if [ ! -d "$skia_library_dir" ] ; then
|
||||||
|
if ! command -v unzip >/dev/null 2>&1 ; then
|
||||||
|
echo "Error: 'unzip' command line tool is not available in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
unzip -n -d "$skia_dir" "$skia_dir/$skia_file"
|
unzip -n -d "$skia_dir" "$skia_dir/$skia_file"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -331,6 +331,8 @@
|
||||||
<part id="flag_highlight" x="16" y="240" w="16" h="10"/>
|
<part id="flag_highlight" x="16" y="240" w="16" h="10"/>
|
||||||
<part id="drop_pixels_ok" x="176" y="176" w="7" h="8"/>
|
<part id="drop_pixels_ok" x="176" y="176" w="7" h="8"/>
|
||||||
<part id="drop_pixels_ok_selected" x="176" y="184" w="7" h="8"/>
|
<part id="drop_pixels_ok_selected" x="176" y="184" w="7" h="8"/>
|
||||||
|
<part id="drop_pixels_drop" x="184" y="176" w="7" h="8"/>
|
||||||
|
<part id="drop_pixels_drop_selected" x="184" y="184" w="7" h="8"/>
|
||||||
<part id="drop_pixels_cancel" x="192" y="176" w="7" h="8"/>
|
<part id="drop_pixels_cancel" x="192" y="176" w="7" h="8"/>
|
||||||
<part id="drop_pixels_cancel_selected" x="192" y="184" w="7" h="8"/>
|
<part id="drop_pixels_cancel_selected" x="192" y="184" w="7" h="8"/>
|
||||||
<part id="warning_box" x="112" y="80" w="9" h="10"/>
|
<part id="warning_box" x="112" y="80" w="9" h="10"/>
|
||||||
|
|
@ -713,6 +715,7 @@
|
||||||
<background color="window_face"/>
|
<background color="window_face"/>
|
||||||
<background-border part="separator_horz" align="middle"/>
|
<background-border part="separator_horz" align="middle"/>
|
||||||
<text color="separator_label" x="4" align="left middle"/>
|
<text color="separator_label" x="4" align="left middle"/>
|
||||||
|
<text color="disabled" x="4" align="left middle" state="disabled"/>
|
||||||
</style>
|
</style>
|
||||||
<style id="menu_separator" extends="horizontal_separator"/>
|
<style id="menu_separator" extends="horizontal_separator"/>
|
||||||
<style id="separator_in_view" extends="horizontal_separator">
|
<style id="separator_in_view" extends="horizontal_separator">
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -327,6 +327,8 @@
|
||||||
<part id="flag_highlight" x="16" y="240" w="16" h="10"/>
|
<part id="flag_highlight" x="16" y="240" w="16" h="10"/>
|
||||||
<part id="drop_pixels_ok" x="176" y="176" w="7" h="8"/>
|
<part id="drop_pixels_ok" x="176" y="176" w="7" h="8"/>
|
||||||
<part id="drop_pixels_ok_selected" x="176" y="184" w="7" h="8"/>
|
<part id="drop_pixels_ok_selected" x="176" y="184" w="7" h="8"/>
|
||||||
|
<part id="drop_pixels_drop" x="184" y="176" w="7" h="8"/>
|
||||||
|
<part id="drop_pixels_drop_selected" x="184" y="184" w="7" h="8"/>
|
||||||
<part id="drop_pixels_cancel" x="192" y="176" w="7" h="8"/>
|
<part id="drop_pixels_cancel" x="192" y="176" w="7" h="8"/>
|
||||||
<part id="drop_pixels_cancel_selected" x="192" y="184" w="7" h="8"/>
|
<part id="drop_pixels_cancel_selected" x="192" y="184" w="7" h="8"/>
|
||||||
<part id="warning_box" x="112" y="80" w="9" h="10"/>
|
<part id="warning_box" x="112" y="80" w="9" h="10"/>
|
||||||
|
|
@ -706,6 +708,7 @@
|
||||||
<background color="window_face"/>
|
<background color="window_face"/>
|
||||||
<background-border part="separator_horz" align="middle"/>
|
<background-border part="separator_horz" align="middle"/>
|
||||||
<text color="separator_label" x="4" align="left middle"/>
|
<text color="separator_label" x="4" align="left middle"/>
|
||||||
|
<text color="disabled" x="4" align="left middle" state="disabled"/>
|
||||||
</style>
|
</style>
|
||||||
<style id="menu_separator" extends="horizontal_separator"/>
|
<style id="menu_separator" extends="horizontal_separator"/>
|
||||||
<style id="separator_in_view" extends="horizontal_separator">
|
<style id="separator_in_view" extends="horizontal_separator">
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.2 KiB |
|
|
@ -34,14 +34,14 @@
|
||||||
type="spritesheet"
|
type="spritesheet"
|
||||||
descent="2"
|
descent="2"
|
||||||
file="aseprite_font.png">
|
file="aseprite_font.png">
|
||||||
<fallback font="Unicode" size="8" />
|
<fallback font="Unicode" size="14" />
|
||||||
</font>
|
</font>
|
||||||
|
|
||||||
<font name="Aseprite Mini"
|
<font name="Aseprite Mini"
|
||||||
type="spritesheet"
|
type="spritesheet"
|
||||||
descent="1"
|
descent="1"
|
||||||
file="aseprite_mini.png">
|
file="aseprite_mini.png">
|
||||||
<fallback font="Unicode" size="6" />
|
<fallback font="Unicode" size="10" />
|
||||||
</font>
|
</font>
|
||||||
|
|
||||||
</fonts>
|
</fonts>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Aseprite -->
|
<!-- Aseprite -->
|
||||||
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
|
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
|
||||||
<!-- Copyright (C) 2001-2018 David Capello -->
|
<!-- Copyright (C) 2001-2018 David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<!-- Keyboard shortcuts -->
|
<!-- Keyboard shortcuts -->
|
||||||
|
|
@ -371,6 +371,12 @@
|
||||||
<param name="quantity" value="1" />
|
<param name="quantity" value="1" />
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
|
<!-- Main selection actions (apply transformation / undo) -->
|
||||||
|
<key command="DeselectMask" shortcut="Esc" context="Transformation" />
|
||||||
|
<key command="Apply" shortcut="Enter" context="Transformation" />
|
||||||
|
<key command="Apply" shortcut="Enter Pad" context="Transformation" />
|
||||||
|
<key command="Undo" shortcut="Ctrl+Z" mac="Cmd+Z" context="Transformation" />
|
||||||
|
|
||||||
<!-- Move selection with arrows -->
|
<!-- Move selection with arrows -->
|
||||||
<key command="MoveMask" shortcut="Left" context="Selection">
|
<key command="MoveMask" shortcut="Left" context="Selection">
|
||||||
<param name="target" value="content" />
|
<param name="target" value="content" />
|
||||||
|
|
@ -1203,6 +1209,7 @@
|
||||||
|
|
||||||
<menu id="slice_popup_menu">
|
<menu id="slice_popup_menu">
|
||||||
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
|
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
|
||||||
|
<item command="DuplicateSlice" text="@.duplicate" group="slice_popup_duplicate" />
|
||||||
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
|
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,7 @@
|
||||||
<section id="file_selector">
|
<section id="file_selector">
|
||||||
<option id="current_folder" type="std::string" default=""<empty>"" />
|
<option id="current_folder" type="std::string" default=""<empty>"" />
|
||||||
<option id="zoom" type="double" default="1.0" />
|
<option id="zoom" type="double" default="1.0" />
|
||||||
|
<option id="show_hidden" type="bool" default="false" />
|
||||||
</section>
|
</section>
|
||||||
<section id="text_tool">
|
<section id="text_tool">
|
||||||
<option id="font_face" type="std::string" />
|
<option id="font_face" type="std::string" />
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@ AddColor_Background = Background
|
||||||
AddColor_Foreground = Foreground
|
AddColor_Foreground = Foreground
|
||||||
AddColor_Specific = Specific
|
AddColor_Specific = Specific
|
||||||
AdvancedMode = Advanced Mode
|
AdvancedMode = Advanced Mode
|
||||||
|
Apply = Apply
|
||||||
AutocropSprite = Trim Sprite
|
AutocropSprite = Trim Sprite
|
||||||
AutocropSprite_ByGrid = Trim Sprite by Grid
|
AutocropSprite_ByGrid = Trim Sprite by Grid
|
||||||
BackgroundFromLayer = Background from Layer
|
BackgroundFromLayer = Background from Layer
|
||||||
|
|
@ -265,6 +266,7 @@ Despeckle = Despeckle
|
||||||
DeveloperConsole = Developer Console
|
DeveloperConsole = Developer Console
|
||||||
DiscardBrush = Discard Brush
|
DiscardBrush = Discard Brush
|
||||||
DuplicateLayer = Duplicate Layer
|
DuplicateLayer = Duplicate Layer
|
||||||
|
DuplicateSlice = Duplicate Slice
|
||||||
DuplicateSprite = Duplicate Sprite
|
DuplicateSprite = Duplicate Sprite
|
||||||
DuplicateView = Duplicate View
|
DuplicateView = Duplicate View
|
||||||
Exit = Exit
|
Exit = Exit
|
||||||
|
|
@ -581,8 +583,9 @@ rotsprite = RotSprite
|
||||||
pixel_perfect = Pixel-perfect
|
pixel_perfect = Pixel-perfect
|
||||||
linear_gradient = Linear Gradient
|
linear_gradient = Linear Gradient
|
||||||
radial_gradient = Radial Gradient
|
radial_gradient = Radial Gradient
|
||||||
drop_pixel = Drop pixels here (Enter)
|
drop_pixel_and_deselect = Apply transformation and deselect
|
||||||
cancel_drag = Cancel drag and drop (Esc)
|
drop_pixel = Apply transformation and keep selection
|
||||||
|
cancel_drag = Cancel transformation and undo/discard changes
|
||||||
auto_select_layer = Auto Select Layer
|
auto_select_layer = Auto Select Layer
|
||||||
all = All
|
all = All
|
||||||
none = None
|
none = None
|
||||||
|
|
@ -621,6 +624,14 @@ current_layer = Current Layer
|
||||||
first_ref_layer = First Reference Layer
|
first_ref_layer = First Reference Layer
|
||||||
pick = Pick:
|
pick = Pick:
|
||||||
sample = Sample:
|
sample = Sample:
|
||||||
|
position_label = P:
|
||||||
|
rotation_label = R:
|
||||||
|
position_x = X Position
|
||||||
|
position_y = Y Position
|
||||||
|
size_width = Width
|
||||||
|
size_height = Height
|
||||||
|
rotation_angle = Angle
|
||||||
|
rotation_skew = Skew
|
||||||
|
|
||||||
[convolution_matrix]
|
[convolution_matrix]
|
||||||
reload_stock = &Reload Stock
|
reload_stock = &Reload Stock
|
||||||
|
|
@ -765,6 +776,7 @@ pinned_folders = Pinned Folders
|
||||||
recent_folders = Recent Folders
|
recent_folders = Recent Folders
|
||||||
all_formats = All formats
|
all_formats = All formats
|
||||||
all_files = All files
|
all_files = All files
|
||||||
|
show_hidden = Show hidden
|
||||||
|
|
||||||
[filters]
|
[filters]
|
||||||
selected_cels = Selected
|
selected_cels = Selected
|
||||||
|
|
@ -816,6 +828,7 @@ same_in_all_tools = Same in all Tools
|
||||||
opacity = Opacity:
|
opacity = Opacity:
|
||||||
tolerance = Tolerance:
|
tolerance = Tolerance:
|
||||||
show_more = Show more...
|
show_more = Show more...
|
||||||
|
copy_of = {} Copy
|
||||||
|
|
||||||
[general_text]
|
[general_text]
|
||||||
copy = &Copy
|
copy = &Copy
|
||||||
|
|
@ -956,6 +969,7 @@ key_context_move_tool = Move Tool
|
||||||
key_context_freehand_tool = Freehand Tool
|
key_context_freehand_tool = Freehand Tool
|
||||||
key_context_shape_tool = Shape Tool
|
key_context_shape_tool = Shape Tool
|
||||||
key_context_frames_selection = Frames Selection
|
key_context_frames_selection = Frames Selection
|
||||||
|
key_context_transformation = Transformation
|
||||||
copy_selection = Copy Selection
|
copy_selection = Copy Selection
|
||||||
snap_to_grid = Snap To Grid
|
snap_to_grid = Snap To Grid
|
||||||
lock_axis = Lock Axis
|
lock_axis = Lock Axis
|
||||||
|
|
@ -1663,6 +1677,10 @@ from = From:
|
||||||
to = To:
|
to = To:
|
||||||
tolerance = Tolerance:
|
tolerance = Tolerance:
|
||||||
|
|
||||||
|
[duplicate_slice]
|
||||||
|
x_duplicated = Slice "{}" duplicated
|
||||||
|
n_slices_duplicated = {} slice(s) duplicated
|
||||||
|
|
||||||
[remove_slice]
|
[remove_slice]
|
||||||
x_removed = Slice "{}" removed
|
x_removed = Slice "{}" removed
|
||||||
n_slices_removed = {} slice(s) removed
|
n_slices_removed = {} slice(s) removed
|
||||||
|
|
@ -1692,7 +1710,8 @@ title = Save Selection (.msk file)
|
||||||
[script_access]
|
[script_access]
|
||||||
title = Security
|
title = Security
|
||||||
script_label = The following script:
|
script_label = The following script:
|
||||||
file_label = wants to access to this file:
|
file_label = wants to access this file:
|
||||||
|
file_write_label = wants to write to this file:
|
||||||
command_label = wants to execute the following command:
|
command_label = wants to execute the following command:
|
||||||
websocket_label = wants to open a WebSocket connection to this URL:
|
websocket_label = wants to open a WebSocket connection to this URL:
|
||||||
clipboard_label = wants to access the system clipboard
|
clipboard_label = wants to access the system clipboard
|
||||||
|
|
@ -1706,7 +1725,7 @@ allow_load_lib_access = &Allow Load External Library
|
||||||
give_full_access = Give Script Full &Access
|
give_full_access = Give Script Full &Access
|
||||||
stop_script = &Stop Script
|
stop_script = &Stop Script
|
||||||
|
|
||||||
[select_accelerator]
|
[select_shortcut]
|
||||||
title = Keyboard Shortcut
|
title = Keyboard Shortcut
|
||||||
key = Key:
|
key = Key:
|
||||||
clear = Clear
|
clear = Clear
|
||||||
|
|
@ -1746,6 +1765,7 @@ delete_file = Delete file, I've already sent it
|
||||||
|
|
||||||
[slice_popup_menu]
|
[slice_popup_menu]
|
||||||
properties = Slice &Properties...
|
properties = Slice &Properties...
|
||||||
|
duplicate = D&uplicate Slice
|
||||||
delete = &Delete Slice
|
delete = &Delete Slice
|
||||||
|
|
||||||
[slice_properties]
|
[slice_properties]
|
||||||
|
|
@ -1833,6 +1853,18 @@ pixel_scale = Pixel Scale
|
||||||
with_vars = Use CSS3 Variables
|
with_vars = Use CSS3 Variables
|
||||||
generate_html = Generate Sample HTML File
|
generate_html = Generate Sample HTML File
|
||||||
|
|
||||||
|
[shape]
|
||||||
|
fill = Fill
|
||||||
|
stroke = Stroke
|
||||||
|
stroke_width = Stroke Width
|
||||||
|
|
||||||
|
[text_tool]
|
||||||
|
font_family = Font Family
|
||||||
|
font_size = Font Size
|
||||||
|
bold = Bold
|
||||||
|
italic = Italic
|
||||||
|
more_options = More Options
|
||||||
|
|
||||||
[timeline_conf]
|
[timeline_conf]
|
||||||
position = Position:
|
position = Position:
|
||||||
left = &Left
|
left = &Left
|
||||||
|
|
@ -1895,6 +1927,7 @@ timeline_show = Show Timeline
|
||||||
|
|
||||||
[undo_history]
|
[undo_history]
|
||||||
title = Undo History
|
title = Undo History
|
||||||
|
initial_state = Initial State
|
||||||
|
|
||||||
[user_data]
|
[user_data]
|
||||||
user_data = User Data:
|
user_data = User Data:
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
<combobox id="location" expansive="true" />
|
<combobox id="location" expansive="true" />
|
||||||
<button text="" id="refresh_button" style="refresh_button"
|
<button text="" id="refresh_button" style="refresh_button"
|
||||||
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
|
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
|
||||||
|
<check id="show_hidden_check" text="@.show_hidden" />
|
||||||
</box>
|
</box>
|
||||||
<vbox id="file_view_placeholder" expansive="true" />
|
<vbox id="file_view_placeholder" expansive="true" />
|
||||||
<grid columns="2">
|
<grid columns="2">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<!-- Aseprite -->
|
<!-- Aseprite -->
|
||||||
|
<!-- Copyright (C) 2025 by Igara Studio S.A. -->
|
||||||
<!-- Copyright (C) 2001-2016 by David Capello -->
|
<!-- Copyright (C) 2001-2016 by David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<window id="select_accelerator" text="@.title">
|
<window id="select_shortcut" text="@.title">
|
||||||
<vbox expansive="true">
|
<vbox expansive="true">
|
||||||
<grid columns="3">
|
<grid columns="3">
|
||||||
<label text="@.key" />
|
<label text="@.key" />
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Aseprite -->
|
<!-- Aseprite -->
|
||||||
<!-- Copyright (C) 2019-2024 Igara Studio S.A. -->
|
<!-- Copyright (C) 2019-2025 Igara Studio S.A. -->
|
||||||
<!-- Copyright (C) 2017-2018 David Capello -->
|
<!-- Copyright (C) 2017-2018 David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<window id="slice_properties" text="@.title" help="slices#slice-properties">
|
<window id="slice_properties" text="@.title" help="slices#slice-properties">
|
||||||
<vbox>
|
<vbox expansive="true">
|
||||||
<grid id="properties_grid" columns="3">
|
<grid id="properties_grid" columns="3">
|
||||||
<label id="label1" text="@.name" />
|
<label id="label1" text="@.name" />
|
||||||
<entry id="name" maxsize="256" magnet="true" cell_align="horizontal" expansive="true" />
|
<entry id="name" maxsize="256" magnet="true" cell_align="horizontal" expansive="true" />
|
||||||
<button id="user_data" icon="icon_user_data" maxsize="32" tooltip="@.user_data_tooltip" />
|
<button id="user_data" icon="icon_user_data" maxsize="32" tooltip="@.user_data_tooltip" />
|
||||||
</grid>
|
</grid>
|
||||||
<grid columns="2">
|
<grid columns="3" expansive="true">
|
||||||
<separator horizontal="true" cell_hspan="2" />
|
<separator horizontal="true" cell_hspan="3" />
|
||||||
|
|
||||||
<box />
|
<box />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
<label text="@.width" />
|
<label text="@.width" />
|
||||||
<label text="@.height" />
|
<label text="@.height" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller cell_align="horizontal" />
|
||||||
|
|
||||||
<label text="@.bounds" />
|
<label text="@.bounds" />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
<expr id="bounds_w" />
|
<expr id="bounds_w" />
|
||||||
<expr id="bounds_h" />
|
<expr id="bounds_h" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller />
|
||||||
|
|
||||||
<check text="@.center" id="center" />
|
<check text="@.center" id="center" />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
|
|
@ -36,16 +38,18 @@
|
||||||
<expr id="center_w" />
|
<expr id="center_w" />
|
||||||
<expr id="center_h" />
|
<expr id="center_h" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller />
|
||||||
|
|
||||||
<check text="@.pivot" id="pivot" />
|
<check text="@.pivot" id="pivot" />
|
||||||
<hbox>
|
<hbox>
|
||||||
<expr id="pivot_x" />
|
<expr id="pivot_x" />
|
||||||
<expr id="pivot_y" />
|
<expr id="pivot_y" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller />
|
||||||
|
|
||||||
<separator horizontal="true" cell_hspan="2" />
|
<boxfiller cell_align="vertical" cell_hspan="3" />
|
||||||
|
<separator horizontal="true" cell_hspan="3" cell_align="horizontal" />
|
||||||
<hbox cell_hspan="2">
|
<hbox cell_hspan="3">
|
||||||
<boxfiller />
|
<boxfiller />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Aseprite -->
|
<!-- Aseprite -->
|
||||||
<!-- Copyright (C) 2019-2021 Igara Studio S.A. -->
|
<!-- Copyright (C) 2019-2025 Igara Studio S.A. -->
|
||||||
<!-- Copyright (C) 2015-2018 David Capello -->
|
<!-- Copyright (C) 2015-2018 David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<window id="tag_properties" text="@.title">
|
<window id="tag_properties" text="@.title">
|
||||||
|
|
@ -23,13 +23,14 @@
|
||||||
<check text="@.repeat" id="limit_repeat" />
|
<check text="@.repeat" id="limit_repeat" />
|
||||||
<vbox id="repeat_placeholder" cell_hspan="2" />
|
<vbox id="repeat_placeholder" cell_hspan="2" />
|
||||||
</grid>
|
</grid>
|
||||||
|
<boxfiller />
|
||||||
<grid columns="2">
|
<grid columns="2">
|
||||||
<separator horizontal="true" cell_hspan="2" minwidth="180" />
|
<separator horizontal="true" cell_align="horizontal" cell_hspan="2" minwidth="180" />
|
||||||
|
|
||||||
<box horizontal="true" homogeneous="true" cell_hspan="2" cell_align="right">
|
<hbox homogeneous="true" cell_hspan="2" cell_align="right">
|
||||||
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
||||||
<button text="@general.cancel" closewindow="true" />
|
<button text="@general.cancel" closewindow="true" />
|
||||||
</box>
|
</hbox>
|
||||||
</grid>
|
</grid>
|
||||||
</vbox>
|
</vbox>
|
||||||
</window>
|
</window>
|
||||||
|
|
|
||||||
2
laf
2
laf
|
|
@ -1 +1 @@
|
||||||
Subproject commit 4f005f84a34cecc55600483e4403b238d5cba466
|
Subproject commit 3f1f86cc734443ba5c72d25c72cdf41ca208e9fe
|
||||||
|
|
@ -180,8 +180,8 @@ if(ENABLE_ASEPRITE_EXE)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(main_resources
|
set(main_resources
|
||||||
main/resources_win32.rc
|
main/win/resources_win32.rc
|
||||||
main/settings.manifest)
|
main/win/settings.manifest)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(${main_target}
|
add_executable(${main_target}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,10 @@ In Debug mode (`_DEBUG`):
|
||||||
* [`TRACEARGS`](https://github.com/aseprite/laf/blob/f3222bdee2d21556e9da55343e73803c730ecd97/base/debug.h#L40):
|
* [`TRACEARGS`](https://github.com/aseprite/laf/blob/f3222bdee2d21556e9da55343e73803c730ecd97/base/debug.h#L40):
|
||||||
in debug mode, it prints in the terminal/console each given argument
|
in debug mode, it prints in the terminal/console each given argument
|
||||||
|
|
||||||
|
In release mode you can use a similar function:
|
||||||
|
|
||||||
|
* `PRINTARGS` prints in the terminal/console each given argument
|
||||||
|
|
||||||
# Detect Platform
|
# Detect Platform
|
||||||
|
|
||||||
You can check the platform using some `laf` macros:
|
You can check the platform using some `laf` macros:
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ add_custom_command(
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${output_fn}.tmp ${output_fn}
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${output_fn}.tmp ${output_fn}
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
MAIN_DEPENDENCY ${strings_en_ini}
|
MAIN_DEPENDENCY ${strings_en_ini}
|
||||||
DEPENDS ${GEN_DEP})
|
DEPENDS ${GEN_DEP} commands/commands_list.h)
|
||||||
list(APPEND generated_files ${output_fn})
|
list(APPEND generated_files ${output_fn})
|
||||||
|
|
||||||
# Check translations
|
# Check translations
|
||||||
|
|
@ -108,7 +108,7 @@ endif()
|
||||||
# This defines a specific webp decoding utility function for using
|
# This defines a specific webp decoding utility function for using
|
||||||
# in Windows when dragging and dropping images that are stored as
|
# in Windows when dragging and dropping images that are stored as
|
||||||
# webp files (like Chrome does).
|
# webp files (like Chrome does).
|
||||||
if(WIN32 AND ENABLE_WEBP)
|
if(WIN32 AND ENABLE_WEBP AND LAF_BACKEND STREQUAL "skia")
|
||||||
target_sources(app-lib PRIVATE util/decode_webp.cpp)
|
target_sources(app-lib PRIVATE util/decode_webp.cpp)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -290,7 +290,6 @@ target_sources(app-lib PRIVATE
|
||||||
cmd/copy_region.cpp
|
cmd/copy_region.cpp
|
||||||
cmd/crop_cel.cpp
|
cmd/crop_cel.cpp
|
||||||
cmd/deselect_mask.cpp
|
cmd/deselect_mask.cpp
|
||||||
cmd/drop_on_timeline.cpp
|
|
||||||
cmd/flatten_layers.cpp
|
cmd/flatten_layers.cpp
|
||||||
cmd/flip_image.cpp
|
cmd/flip_image.cpp
|
||||||
cmd/flip_mask.cpp
|
cmd/flip_mask.cpp
|
||||||
|
|
@ -369,6 +368,7 @@ target_sources(app-lib PRIVATE
|
||||||
color_picker.cpp
|
color_picker.cpp
|
||||||
color_spaces.cpp
|
color_spaces.cpp
|
||||||
color_utils.cpp
|
color_utils.cpp
|
||||||
|
commands/apply.cpp
|
||||||
commands/cmd_about.cpp
|
commands/cmd_about.cpp
|
||||||
commands/cmd_add_color.cpp
|
commands/cmd_add_color.cpp
|
||||||
commands/cmd_advanced_mode.cpp
|
commands/cmd_advanced_mode.cpp
|
||||||
|
|
@ -394,6 +394,7 @@ target_sources(app-lib PRIVATE
|
||||||
commands/cmd_deselect_mask.cpp
|
commands/cmd_deselect_mask.cpp
|
||||||
commands/cmd_discard_brush.cpp
|
commands/cmd_discard_brush.cpp
|
||||||
commands/cmd_duplicate_layer.cpp
|
commands/cmd_duplicate_layer.cpp
|
||||||
|
commands/cmd_duplicate_slice.cpp
|
||||||
commands/cmd_duplicate_sprite.cpp
|
commands/cmd_duplicate_sprite.cpp
|
||||||
commands/cmd_duplicate_view.cpp
|
commands/cmd_duplicate_view.cpp
|
||||||
commands/cmd_enter_license.cpp
|
commands/cmd_enter_license.cpp
|
||||||
|
|
@ -526,6 +527,7 @@ target_sources(app-lib PRIVATE
|
||||||
context_flags.cpp
|
context_flags.cpp
|
||||||
doc.cpp
|
doc.cpp
|
||||||
doc_api.cpp
|
doc_api.cpp
|
||||||
|
doc_api_dnd_helper.cpp
|
||||||
doc_diff.cpp
|
doc_diff.cpp
|
||||||
doc_exporter.cpp
|
doc_exporter.cpp
|
||||||
doc_range_ops.cpp
|
doc_range_ops.cpp
|
||||||
|
|
@ -669,7 +671,7 @@ target_sources(app-lib PRIVATE
|
||||||
ui/rgbmap_algorithm_selector.cpp
|
ui/rgbmap_algorithm_selector.cpp
|
||||||
ui/sampling_selector.cpp
|
ui/sampling_selector.cpp
|
||||||
ui/search_entry.cpp
|
ui/search_entry.cpp
|
||||||
ui/select_accelerator.cpp
|
ui/select_shortcut.cpp
|
||||||
ui/selection_mode_field.cpp
|
ui/selection_mode_field.cpp
|
||||||
ui/skin/skin_part.cpp
|
ui/skin/skin_part.cpp
|
||||||
ui/skin/skin_property.cpp
|
ui/skin/skin_property.cpp
|
||||||
|
|
@ -684,6 +686,7 @@ target_sources(app-lib PRIVATE
|
||||||
ui/tile_button.cpp
|
ui/tile_button.cpp
|
||||||
ui/tileset_selector.cpp
|
ui/tileset_selector.cpp
|
||||||
ui/timeline/ani_controls.cpp
|
ui/timeline/ani_controls.cpp
|
||||||
|
ui/timeline/doc_providers.cpp
|
||||||
ui/timeline/timeline.cpp
|
ui/timeline/timeline.cpp
|
||||||
ui/toolbar.cpp
|
ui/toolbar.cpp
|
||||||
ui/user_data_view.cpp
|
ui/user_data_view.cpp
|
||||||
|
|
@ -706,6 +709,7 @@ target_sources(app-lib PRIVATE
|
||||||
util/layer_utils.cpp
|
util/layer_utils.cpp
|
||||||
util/msk_file.cpp
|
util/msk_file.cpp
|
||||||
util/new_image_from_mask.cpp
|
util/new_image_from_mask.cpp
|
||||||
|
util/open_file_job.cpp
|
||||||
util/pal_ops.cpp
|
util/pal_ops.cpp
|
||||||
util/pic_file.cpp
|
util/pic_file.cpp
|
||||||
util/pixel_ratio.cpp
|
util/pixel_ratio.cpp
|
||||||
|
|
@ -713,6 +717,7 @@ target_sources(app-lib PRIVATE
|
||||||
util/render_text.cpp
|
util/render_text.cpp
|
||||||
util/resize_image.cpp
|
util/resize_image.cpp
|
||||||
util/shader_helpers.cpp
|
util/shader_helpers.cpp
|
||||||
|
util/slice_utils.cpp
|
||||||
util/tile_flags_utils.cpp
|
util/tile_flags_utils.cpp
|
||||||
util/tileset_utils.cpp
|
util/tileset_utils.cpp
|
||||||
util/wrap_point.cpp
|
util/wrap_point.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
#include "os/x11/system.h"
|
#include "os/x11/system.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if ENABLE_WEBP && LAF_WINDOWS
|
#if ENABLE_WEBP && LAF_WINDOWS && LAF_SKIA
|
||||||
#include "app/util/decode_webp.h"
|
#include "app/util/decode_webp.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
@ -485,7 +485,7 @@ void App::run(const bool runGuiManager)
|
||||||
// How to interpret one finger on Windows tablets.
|
// How to interpret one finger on Windows tablets.
|
||||||
manager->display()->nativeWindow()->setInterpretOneFingerGestureAsMouseMovement(
|
manager->display()->nativeWindow()->setInterpretOneFingerGestureAsMouseMovement(
|
||||||
preferences().experimental.oneFingerAsMouseMovement());
|
preferences().experimental.oneFingerAsMouseMovement());
|
||||||
#if ENABLE_WEBP
|
#if ENABLE_WEBP && LAF_SKIA
|
||||||
// In Windows we use a custom webp decoder for drag & drop operations.
|
// In Windows we use a custom webp decoder for drag & drop operations.
|
||||||
os::set_decode_webp(util::decode_webp);
|
os::set_decode_webp(util::decode_webp);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,11 @@ bool can_call_global_shortcut(const AppMenuItem::Native* native)
|
||||||
(focus == nullptr || focus->type() != ui::kEntryWidget ||
|
(focus == nullptr || focus->type() != ui::kEntryWidget ||
|
||||||
!is_text_entry_shortcut(native->shortcut)) &&
|
!is_text_entry_shortcut(native->shortcut)) &&
|
||||||
(native->keyContext == KeyContext::Any ||
|
(native->keyContext == KeyContext::Any ||
|
||||||
native->keyContext == KeyboardShortcuts::instance()->getCurrentKeyContext());
|
native->keyContext == KeyboardShortcuts::getCurrentKeyContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO this should be on "she" library (or we should use
|
// TODO this should be on laf-os library (or we should use
|
||||||
// os::Shortcut instead of ui::Accelerators)
|
// os::Shortcut instead of ui::Shortcuts)
|
||||||
int from_scancode_to_unicode(KeyScancode scancode)
|
int from_scancode_to_unicode(KeyScancode scancode)
|
||||||
{
|
{
|
||||||
static int map[] = {
|
static int map[] = {
|
||||||
|
|
@ -284,22 +284,21 @@ void destroy_menu_item(ui::Widget* item)
|
||||||
|
|
||||||
os::Shortcut get_os_shortcut_from_key(const Key* key)
|
os::Shortcut get_os_shortcut_from_key(const Key* key)
|
||||||
{
|
{
|
||||||
if (key && !key->accels().empty()) {
|
if (key && !key->shortcuts().empty()) {
|
||||||
const ui::Accelerator& accel = key->accels().front();
|
const ui::Shortcut& shortcut = key->shortcuts().front();
|
||||||
|
|
||||||
#if LAF_MACOS
|
#if LAF_MACOS
|
||||||
// Shortcuts with spacebar as modifier do not work well in macOS
|
// Shortcuts with spacebar as modifier do not work well in macOS
|
||||||
// (they will be called when the space bar is unpressed too).
|
// (they will be called when the space bar is unpressed too).
|
||||||
if ((accel.modifiers() & ui::kKeySpaceModifier) == ui::kKeySpaceModifier)
|
if ((shortcut.modifiers() & ui::kKeySpaceModifier) == ui::kKeySpaceModifier)
|
||||||
return os::Shortcut();
|
return os::Shortcut();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return os::Shortcut(
|
return os::Shortcut((shortcut.unicodeChar() ? shortcut.unicodeChar() :
|
||||||
(accel.unicodeChar() ? accel.unicodeChar() : from_scancode_to_unicode(accel.scancode())),
|
from_scancode_to_unicode(shortcut.scancode())),
|
||||||
accel.modifiers());
|
shortcut.modifiers());
|
||||||
}
|
}
|
||||||
else
|
return {};
|
||||||
return os::Shortcut();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppMenus* AppMenus::s_instance = nullptr;
|
AppMenus* AppMenus::s_instance = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -1,397 +0,0 @@
|
||||||
// Aseprite
|
|
||||||
// Copyright (C) 2024 Igara Studio S.A.
|
|
||||||
//
|
|
||||||
// This program is distributed under the terms of
|
|
||||||
// the End-User License Agreement for Aseprite.
|
|
||||||
|
|
||||||
#ifdef HAVE_CONFIG_H
|
|
||||||
#include "config.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "app/cmd/drop_on_timeline.h"
|
|
||||||
|
|
||||||
#include "app/cmd/add_layer.h"
|
|
||||||
#include "app/cmd/move_cel.h"
|
|
||||||
#include "app/cmd/set_pixel_format.h"
|
|
||||||
#include "app/console.h"
|
|
||||||
#include "app/context_flags.h"
|
|
||||||
#include "app/doc.h"
|
|
||||||
#include "app/doc_event.h"
|
|
||||||
#include "app/file/file.h"
|
|
||||||
#include "app/tx.h"
|
|
||||||
#include "app/util/layer_utils.h"
|
|
||||||
#include "app/util/open_file_job.h"
|
|
||||||
#include "base/serialization.h"
|
|
||||||
#include "doc/layer_io.h"
|
|
||||||
#include "doc/layer_list.h"
|
|
||||||
#include "doc/subobjects_io.h"
|
|
||||||
#include "render/dithering.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace app { namespace cmd {
|
|
||||||
|
|
||||||
using namespace base::serialization::little_endian;
|
|
||||||
|
|
||||||
DropOnTimeline::DropOnTimeline(app::Doc* doc,
|
|
||||||
doc::frame_t frame,
|
|
||||||
doc::layer_t layerIndex,
|
|
||||||
InsertionPoint insert,
|
|
||||||
DroppedOn droppedOn,
|
|
||||||
const base::paths& paths)
|
|
||||||
: WithDocument(doc)
|
|
||||||
, m_size(0)
|
|
||||||
, m_paths(paths)
|
|
||||||
, m_frame(frame)
|
|
||||||
, m_layerIndex(layerIndex)
|
|
||||||
, m_insert(insert)
|
|
||||||
, m_droppedOn(droppedOn)
|
|
||||||
{
|
|
||||||
ASSERT(m_layerIndex >= 0);
|
|
||||||
for (const auto& path : m_paths)
|
|
||||||
m_size += path.size();
|
|
||||||
|
|
||||||
// Zero layers stored.
|
|
||||||
write32(m_stream, 0);
|
|
||||||
m_size += sizeof(uint32_t);
|
|
||||||
}
|
|
||||||
|
|
||||||
DropOnTimeline::DropOnTimeline(app::Doc* doc,
|
|
||||||
doc::frame_t frame,
|
|
||||||
doc::layer_t layerIndex,
|
|
||||||
InsertionPoint insert,
|
|
||||||
DroppedOn droppedOn,
|
|
||||||
const doc::ImageRef& image)
|
|
||||||
: WithDocument(doc)
|
|
||||||
, m_size(0)
|
|
||||||
, m_image(image)
|
|
||||||
, m_frame(frame)
|
|
||||||
, m_layerIndex(layerIndex)
|
|
||||||
, m_insert(insert)
|
|
||||||
, m_droppedOn(droppedOn)
|
|
||||||
{
|
|
||||||
ASSERT(m_layerIndex >= 0);
|
|
||||||
|
|
||||||
// Zero layers stored.
|
|
||||||
write32(m_stream, 0);
|
|
||||||
m_size += sizeof(uint32_t);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::onExecute()
|
|
||||||
{
|
|
||||||
Doc* destDoc = document();
|
|
||||||
m_previousTotalFrames = destDoc->sprite()->totalFrames();
|
|
||||||
|
|
||||||
int docsProcessed = 0;
|
|
||||||
while (hasPendingWork()) {
|
|
||||||
std::unique_ptr<Doc> srcDoc;
|
|
||||||
if (!getNextDoc(srcDoc))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (srcDoc) {
|
|
||||||
docsProcessed++;
|
|
||||||
// If source document doesn't match the destination document's color
|
|
||||||
// mode, change it.
|
|
||||||
if (srcDoc->colorMode() != destDoc->colorMode()) {
|
|
||||||
// Execute in a source doc transaction because we don't need undo/redo
|
|
||||||
// this.
|
|
||||||
Tx tx(srcDoc.get());
|
|
||||||
tx(new cmd::SetPixelFormat(srcDoc->sprite(),
|
|
||||||
destDoc->sprite()->pixelFormat(),
|
|
||||||
render::Dithering(),
|
|
||||||
Preferences::instance().quantization.rgbmapAlgorithm(),
|
|
||||||
nullptr,
|
|
||||||
nullptr,
|
|
||||||
FitCriteria::DEFAULT));
|
|
||||||
tx.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is only one source document to process and it has a cel that
|
|
||||||
// can be moved, then move the cel from the source doc into the
|
|
||||||
// destination doc's selected frame.
|
|
||||||
const bool isJustOneDoc = (docsProcessed == 1 && !hasPendingWork());
|
|
||||||
if (isJustOneDoc && canMoveCelFrom(srcDoc.get())) {
|
|
||||||
auto* srcLayer = static_cast<LayerImage*>(srcDoc->sprite()->firstLayer());
|
|
||||||
auto* destLayer = static_cast<LayerImage*>(destDoc->sprite()->allLayers()[m_layerIndex]);
|
|
||||||
executeAndAdd(new MoveCel(srcLayer, 0, destLayer, m_frame, false));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no room for the source frames, add frames to the
|
|
||||||
// destination sprite.
|
|
||||||
if (m_frame + srcDoc->sprite()->totalFrames() > destDoc->sprite()->totalFrames()) {
|
|
||||||
destDoc->sprite()->setTotalFrames(m_frame + srcDoc->sprite()->totalFrames());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save dropped layers from source document.
|
|
||||||
saveDroppedLayers(srcDoc->sprite()->root()->layers(), destDoc->sprite());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_droppedLayersIds.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
destDoc->sprite()->incrementVersion();
|
|
||||||
destDoc->incrementVersion();
|
|
||||||
|
|
||||||
insertDroppedLayers();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::onUndo()
|
|
||||||
{
|
|
||||||
CmdSequence::onUndo();
|
|
||||||
|
|
||||||
if (m_droppedLayersIds.empty()) {
|
|
||||||
notifyGeneralUpdate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Doc* doc = document();
|
|
||||||
frame_t currentTotalFrames = doc->sprite()->totalFrames();
|
|
||||||
|
|
||||||
for (auto id : m_droppedLayersIds) {
|
|
||||||
auto* layer = doc::get<Layer>(id);
|
|
||||||
ASSERT(layer);
|
|
||||||
if (layer) {
|
|
||||||
DocEvent ev(doc);
|
|
||||||
ev.sprite(layer->sprite());
|
|
||||||
ev.layer(layer);
|
|
||||||
doc->notify_observers<DocEvent&>(&DocObserver::onBeforeRemoveLayer, ev);
|
|
||||||
|
|
||||||
LayerGroup* group = layer->parent();
|
|
||||||
group->removeLayer(layer);
|
|
||||||
group->incrementVersion();
|
|
||||||
group->sprite()->incrementVersion();
|
|
||||||
|
|
||||||
doc->notify_observers<DocEvent&>(&DocObserver::onAfterRemoveLayer, ev);
|
|
||||||
|
|
||||||
delete layer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doc->sprite()->setTotalFrames(m_previousTotalFrames);
|
|
||||||
doc->sprite()->incrementVersion();
|
|
||||||
m_previousTotalFrames = currentTotalFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::onRedo()
|
|
||||||
{
|
|
||||||
CmdSequence::onRedo();
|
|
||||||
|
|
||||||
if (m_droppedLayersIds.empty()) {
|
|
||||||
notifyGeneralUpdate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Doc* doc = document();
|
|
||||||
frame_t currentTotalFrames = doc->sprite()->totalFrames();
|
|
||||||
doc->sprite()->setTotalFrames(m_previousTotalFrames);
|
|
||||||
doc->sprite()->incrementVersion();
|
|
||||||
m_previousTotalFrames = currentTotalFrames;
|
|
||||||
|
|
||||||
insertDroppedLayers();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::setupInsertionLayer(Layer*& layer, LayerGroup*& group)
|
|
||||||
{
|
|
||||||
const LayerList& allLayers = document()->sprite()->allLayers();
|
|
||||||
layer = allLayers[m_layerIndex];
|
|
||||||
if (m_insert == InsertionPoint::BeforeLayer && layer->isGroup()) {
|
|
||||||
group = static_cast<LayerGroup*>(layer);
|
|
||||||
// The user is trying to drop layers into an empty group, so there is no after
|
|
||||||
// nor before layer...
|
|
||||||
if (group->layersCount() == 0) {
|
|
||||||
layer = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
layer = group->lastLayer();
|
|
||||||
m_insert = InsertionPoint::AfterLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
group = layer->parent();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DropOnTimeline::hasPendingWork()
|
|
||||||
{
|
|
||||||
return m_image || !m_paths.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DropOnTimeline::getNextDocFromImage(std::unique_ptr<Doc>& srcDoc)
|
|
||||||
{
|
|
||||||
if (!m_image)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
Sprite* sprite = new Sprite(m_image->spec(), 256);
|
|
||||||
LayerImage* layer = new LayerImage(sprite);
|
|
||||||
sprite->root()->addLayer(layer);
|
|
||||||
Cel* cel = new Cel(0, m_image);
|
|
||||||
layer->addCel(cel);
|
|
||||||
srcDoc = std::make_unique<Doc>(sprite);
|
|
||||||
m_image = nullptr;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DropOnTimeline::getNextDocFromPaths(std::unique_ptr<Doc>& srcDoc)
|
|
||||||
{
|
|
||||||
Console console;
|
|
||||||
Context* context = document()->context();
|
|
||||||
int flags = FILE_LOAD_DATA_FILE | FILE_LOAD_AVOID_BACKGROUND_LAYER | FILE_LOAD_CREATE_PALETTE |
|
|
||||||
FILE_LOAD_SEQUENCE_YES;
|
|
||||||
|
|
||||||
std::unique_ptr<FileOp> fop(FileOp::createLoadDocumentOperation(context, m_paths.front(), flags));
|
|
||||||
// Remove the path that is currently being processed
|
|
||||||
m_paths.erase(m_paths.begin());
|
|
||||||
|
|
||||||
// Do nothing (the user cancelled or something like that)
|
|
||||||
if (!fop)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (fop->hasError()) {
|
|
||||||
console.printf(fop->error().c_str());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
base::paths fopFilenames;
|
|
||||||
fop->getFilenameList(fopFilenames);
|
|
||||||
// Remove paths that will be loaded by the current file operation.
|
|
||||||
for (const auto& filename : fopFilenames) {
|
|
||||||
auto it = std::find(m_paths.begin(), m_paths.end(), filename);
|
|
||||||
if (it != m_paths.end())
|
|
||||||
m_paths.erase(it);
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenFileJob task(fop.get(), true);
|
|
||||||
task.showProgressWindow();
|
|
||||||
|
|
||||||
// Post-load processing, it is called from the GUI because may require user intervention.
|
|
||||||
fop->postLoad();
|
|
||||||
|
|
||||||
// Show any error
|
|
||||||
if (fop->hasError() && !fop->isStop())
|
|
||||||
console.printf(fop->error().c_str());
|
|
||||||
|
|
||||||
srcDoc.reset(fop->releaseDocument());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DropOnTimeline::getNextDoc(std::unique_ptr<Doc>& srcDoc)
|
|
||||||
{
|
|
||||||
if (m_image == nullptr && !m_paths.empty())
|
|
||||||
return getNextDocFromPaths(srcDoc);
|
|
||||||
|
|
||||||
return getNextDocFromImage(srcDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::storeDroppedLayerIds(const Layer* layer)
|
|
||||||
{
|
|
||||||
if (layer->isGroup()) {
|
|
||||||
const auto* group = static_cast<const LayerGroup*>(layer);
|
|
||||||
for (auto* child : group->layers())
|
|
||||||
storeDroppedLayerIds(child);
|
|
||||||
|
|
||||||
m_droppedLayersIds.push_back(group->id());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_droppedLayersIds.push_back(layer->id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::saveDroppedLayers(const LayerList& layers, Sprite* sprite)
|
|
||||||
{
|
|
||||||
size_t start = m_stream.tellp();
|
|
||||||
|
|
||||||
// Calculate the new number of layers.
|
|
||||||
m_stream.seekg(0);
|
|
||||||
uint32_t nLayers = read32(m_stream) + layers.size();
|
|
||||||
|
|
||||||
// Flat list of all the dropped layers.
|
|
||||||
LayerList allDroppedLayers;
|
|
||||||
// Write number of layers (at the beginning of the stream).
|
|
||||||
m_stream.seekp(0);
|
|
||||||
write32(m_stream, nLayers);
|
|
||||||
// Move to where we must start writing.
|
|
||||||
m_stream.seekp(start);
|
|
||||||
for (auto it = layers.cbegin(); it != layers.cend(); ++it) {
|
|
||||||
auto* layer = *it;
|
|
||||||
// TODO: If we could "relocate" a layer from the source document to the
|
|
||||||
// destination document we could avoid making a copy here.
|
|
||||||
std::unique_ptr<Layer> layerCopy(copy_layer_with_sprite(layer, sprite));
|
|
||||||
layerCopy->displaceFrames(0, m_frame);
|
|
||||||
|
|
||||||
write_layer(m_stream, layerCopy.get());
|
|
||||||
|
|
||||||
storeDroppedLayerIds(layerCopy.get());
|
|
||||||
}
|
|
||||||
size_t end = m_stream.tellp();
|
|
||||||
m_size += end - start;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::insertDroppedLayers()
|
|
||||||
{
|
|
||||||
// Layer used as a reference to determine if the dropped layers will be
|
|
||||||
// inserted after or before it.
|
|
||||||
Layer* refLayer = nullptr;
|
|
||||||
// Parent group of the reference layer layer.
|
|
||||||
LayerGroup* group = nullptr;
|
|
||||||
// Keep track of the current insertion point.
|
|
||||||
InsertionPoint insert = m_insert;
|
|
||||||
|
|
||||||
setupInsertionLayer(refLayer, group);
|
|
||||||
|
|
||||||
SubObjectsFromSprite io(group->sprite());
|
|
||||||
m_stream.seekg(0);
|
|
||||||
auto nLayers = read32(m_stream);
|
|
||||||
for (int i = 0; i < nLayers; ++i) {
|
|
||||||
auto* layer = read_layer(m_stream, &io);
|
|
||||||
|
|
||||||
if (!refLayer) {
|
|
||||||
group->addLayer(layer);
|
|
||||||
refLayer = layer;
|
|
||||||
insert = InsertionPoint::AfterLayer;
|
|
||||||
}
|
|
||||||
else if (insert == InsertionPoint::AfterLayer) {
|
|
||||||
group->insertLayer(layer, refLayer);
|
|
||||||
refLayer = layer;
|
|
||||||
}
|
|
||||||
else if (insert == InsertionPoint::BeforeLayer) {
|
|
||||||
group->insertLayerBefore(layer, refLayer);
|
|
||||||
refLayer = layer;
|
|
||||||
insert = InsertionPoint::AfterLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
group->incrementVersion();
|
|
||||||
group->sprite()->incrementVersion();
|
|
||||||
|
|
||||||
Doc* doc = static_cast<Doc*>(group->sprite()->document());
|
|
||||||
DocEvent ev(doc);
|
|
||||||
ev.sprite(group->sprite());
|
|
||||||
ev.layer(layer);
|
|
||||||
doc->notify_observers<DocEvent&>(&DocObserver::onAddLayer, ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if the document srcDoc has a cel that can be moved.
|
|
||||||
// The cel from the srcDoc can be moved only when all of the following
|
|
||||||
// conditions are met:
|
|
||||||
// * Drop took place in a cel.
|
|
||||||
// * Source doc has only one layer with just one frame.
|
|
||||||
// * The layer from the source doc and the destination cel's layer are both
|
|
||||||
// Image layers.
|
|
||||||
// Otherwise this function returns false.
|
|
||||||
bool DropOnTimeline::canMoveCelFrom(app::Doc* srcDoc)
|
|
||||||
{
|
|
||||||
auto* srcLayer = srcDoc->sprite()->firstLayer();
|
|
||||||
auto* destLayer = document()->sprite()->allLayers()[m_layerIndex];
|
|
||||||
return m_droppedOn == DroppedOn::Cel && srcDoc->sprite()->allLayersCount() == 1 &&
|
|
||||||
srcDoc->sprite()->totalFrames() == 1 && srcLayer->isImage() && destLayer->isImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropOnTimeline::notifyGeneralUpdate()
|
|
||||||
{
|
|
||||||
Doc* doc = document();
|
|
||||||
if (!doc)
|
|
||||||
return;
|
|
||||||
|
|
||||||
doc->notifyGeneralUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
}} // namespace app::cmd
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
// Aseprite
|
|
||||||
// Copyright (C) 2024 Igara Studio S.A.
|
|
||||||
//
|
|
||||||
// This program is distributed under the terms of
|
|
||||||
// the End-User License Agreement for Aseprite.
|
|
||||||
|
|
||||||
#ifndef APP_CMD_drop_on_timeline_H_INCLUDED
|
|
||||||
#define APP_CMD_drop_on_timeline_H_INCLUDED
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "app/cmd/with_document.h"
|
|
||||||
#include "app/cmd_sequence.h"
|
|
||||||
#include "app/doc_observer.h"
|
|
||||||
#include "base/paths.h"
|
|
||||||
#include "doc/frame.h"
|
|
||||||
#include "doc/image_ref.h"
|
|
||||||
#include "doc/layer.h"
|
|
||||||
#include "doc/layer_list.h"
|
|
||||||
|
|
||||||
namespace app { namespace cmd {
|
|
||||||
|
|
||||||
class DropOnTimeline : public CmdSequence,
|
|
||||||
public WithDocument {
|
|
||||||
public:
|
|
||||||
enum class InsertionPoint {
|
|
||||||
BeforeLayer,
|
|
||||||
AfterLayer,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class DroppedOn {
|
|
||||||
Unspecified,
|
|
||||||
Frame,
|
|
||||||
Layer,
|
|
||||||
Cel,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Inserts the layers and frames of the documents pointed by the specified
|
|
||||||
// paths, at the specified frame and before or after the specified layer index.
|
|
||||||
DropOnTimeline(app::Doc* doc,
|
|
||||||
doc::frame_t frame,
|
|
||||||
doc::layer_t layerIndex,
|
|
||||||
InsertionPoint insert,
|
|
||||||
DroppedOn droppedOn,
|
|
||||||
const base::paths& paths);
|
|
||||||
|
|
||||||
// Inserts the image as if it were a document with just one layer and one
|
|
||||||
// frame, at the specified frame and before or after the specified layer index.
|
|
||||||
DropOnTimeline(app::Doc* doc,
|
|
||||||
doc::frame_t frame,
|
|
||||||
doc::layer_t layerIndex,
|
|
||||||
InsertionPoint insert,
|
|
||||||
DroppedOn droppedOn,
|
|
||||||
const doc::ImageRef& image);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void onExecute() override;
|
|
||||||
void onUndo() override;
|
|
||||||
void onRedo() override;
|
|
||||||
size_t onMemSize() const override { return sizeof(*this) + m_size; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void setupInsertionLayer(doc::Layer*& layer, doc::LayerGroup*& group);
|
|
||||||
void insertDroppedLayers();
|
|
||||||
bool canMoveCelFrom(app::Doc* srcDoc);
|
|
||||||
void notifyGeneralUpdate();
|
|
||||||
bool hasPendingWork();
|
|
||||||
// Returns the next document to be processed.
|
|
||||||
// Returns false when the user cancelled the process, or true when the
|
|
||||||
// process must go on.
|
|
||||||
bool getNextDoc(std::unique_ptr<Doc>& srcDoc);
|
|
||||||
bool getNextDocFromImage(std::unique_ptr<Doc>& srcDoc);
|
|
||||||
bool getNextDocFromPaths(std::unique_ptr<Doc>& srcDoc);
|
|
||||||
|
|
||||||
void storeDroppedLayerIds(const doc::Layer* layer);
|
|
||||||
void saveDroppedLayers(const doc::LayerList& layers, doc::Sprite* sprite);
|
|
||||||
|
|
||||||
size_t m_size;
|
|
||||||
base::paths m_paths;
|
|
||||||
doc::ImageRef m_image = nullptr;
|
|
||||||
doc::frame_t m_frame;
|
|
||||||
doc::layer_t m_layerIndex;
|
|
||||||
InsertionPoint m_insert;
|
|
||||||
DroppedOn m_droppedOn;
|
|
||||||
// Serialized dropped layers' data. Used for redo operation.
|
|
||||||
std::stringstream m_stream;
|
|
||||||
// Holds the Object IDs of the dropped layers. Used when determining which
|
|
||||||
// layers should be removed in an undo operation.
|
|
||||||
std::vector<doc::ObjectId> m_droppedLayersIds;
|
|
||||||
// Number of frames the doc had before dropping.
|
|
||||||
doc::frame_t m_previousTotalFrames;
|
|
||||||
};
|
|
||||||
|
|
||||||
}} // namespace app::cmd
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "app/commands/command.h"
|
||||||
|
#include "app/context.h"
|
||||||
|
#include "app/ui/editor/editor.h"
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Depends on the current context/state, used to apply the current
|
||||||
|
// transformation (drop pixels).
|
||||||
|
class ApplyCommand : public Command {
|
||||||
|
public:
|
||||||
|
ApplyCommand();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onExecute(Context* ctx) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
ApplyCommand::ApplyCommand() : Command(CommandId::Apply(), CmdUIOnlyFlag)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyCommand::onExecute(Context* ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->isUIAvailable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto* editor = Editor::activeEditor();
|
||||||
|
if (editor && editor->isMovingPixels())
|
||||||
|
editor->dropMovingPixels();
|
||||||
|
}
|
||||||
|
|
||||||
|
Command* CommandFactory::createApplyCommand()
|
||||||
|
{
|
||||||
|
return new ApplyCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -54,11 +55,11 @@ void AdvancedModeCommand::onExecute(Context* context)
|
||||||
|
|
||||||
if (oldMode == MainWindow::NormalMode && pref.advancedMode.showAlert()) {
|
if (oldMode == MainWindow::NormalMode && pref.advancedMode.showAlert()) {
|
||||||
KeyPtr key = KeyboardShortcuts::instance()->command(this->id().c_str());
|
KeyPtr key = KeyboardShortcuts::instance()->command(this->id().c_str());
|
||||||
if (!key->accels().empty()) {
|
if (!key->shortcuts().empty()) {
|
||||||
app::gen::AdvancedMode window;
|
app::gen::AdvancedMode window;
|
||||||
|
|
||||||
window.warningLabel()->setTextf("You can go back pressing \"%s\" key.",
|
window.warningLabel()->setTextf("You can go back pressing \"%s\" key.",
|
||||||
key->accels().front().toString().c_str());
|
key->shortcuts().front().toString().c_str());
|
||||||
|
|
||||||
window.openWindowInForeground();
|
window.openWindowInForeground();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2023 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -101,12 +101,12 @@ public:
|
||||||
|
|
||||||
if (countCels() > 0) {
|
if (countCels() > 0) {
|
||||||
m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()),
|
m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()),
|
||||||
g_window->propertiesGrid());
|
propertiesGrid());
|
||||||
}
|
}
|
||||||
else if (!m_cel)
|
else if (!m_cel)
|
||||||
m_userDataView.setVisible(false, false);
|
m_userDataView.setVisible(false, false);
|
||||||
|
|
||||||
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
updateFromCel();
|
updateFromCel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +281,7 @@ private:
|
||||||
{
|
{
|
||||||
if (countCels() > 0) {
|
if (countCels() > 0) {
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2025 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "app/cmd/add_slice.h"
|
||||||
|
#include "app/commands/command.h"
|
||||||
|
#include "app/context.h"
|
||||||
|
#include "app/context_access.h"
|
||||||
|
#include "app/context_flags.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
|
#include "app/site.h"
|
||||||
|
#include "app/tx.h"
|
||||||
|
#include "app/ui/status_bar.h"
|
||||||
|
#include "app/util/slice_utils.h"
|
||||||
|
#include "base/convert_to.h"
|
||||||
|
#include "doc/object_id.h"
|
||||||
|
#include "doc/slice.h"
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Moves the given slice by the dx and dy values
|
||||||
|
void offset(Slice* slice, int dx, int dy)
|
||||||
|
{
|
||||||
|
for (auto it = slice->begin(); it != slice->end(); ++it) {
|
||||||
|
auto* sk = (*it).value();
|
||||||
|
gfx::Rect bounds = sk->bounds();
|
||||||
|
bounds.offset(gfx::Point{ dx, dy });
|
||||||
|
sk->setBounds(bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DuplicateSliceCommand : public Command {
|
||||||
|
public:
|
||||||
|
DuplicateSliceCommand();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onLoadParams(const Params& params) override;
|
||||||
|
bool onEnabled(Context* context) override;
|
||||||
|
void onExecute(Context* context) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ObjectId m_sliceId;
|
||||||
|
};
|
||||||
|
|
||||||
|
DuplicateSliceCommand::DuplicateSliceCommand()
|
||||||
|
: Command(CommandId::DuplicateSlice(), CmdRecordableFlag)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void DuplicateSliceCommand::onLoadParams(const Params& params)
|
||||||
|
{
|
||||||
|
std::string id = params.get("id");
|
||||||
|
if (!id.empty())
|
||||||
|
m_sliceId = ObjectId(base::convert_to<doc::ObjectId>(id));
|
||||||
|
else
|
||||||
|
m_sliceId = NullId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DuplicateSliceCommand::onEnabled(Context* context)
|
||||||
|
{
|
||||||
|
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
|
||||||
|
ContextFlags::HasActiveSprite | ContextFlags::HasActiveLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DuplicateSliceCommand::onExecute(Context* context)
|
||||||
|
{
|
||||||
|
std::vector<Slice*> selectedSlices;
|
||||||
|
{
|
||||||
|
const ContextReader reader(context);
|
||||||
|
if (m_sliceId == NullId) {
|
||||||
|
selectedSlices = get_selected_slices(reader.site());
|
||||||
|
if (selectedSlices.empty())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
selectedSlices.push_back(reader.sprite()->slices().getById(m_sliceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextWriter writer(context);
|
||||||
|
Tx tx(writer, "Duplicate Slice");
|
||||||
|
Sprite* sprite = writer.site().sprite();
|
||||||
|
|
||||||
|
Doc* doc = static_cast<Doc*>(sprite->document());
|
||||||
|
doc->notifyBeforeSlicesDuplication();
|
||||||
|
for (auto* s : selectedSlices) {
|
||||||
|
Slice* slice = new Slice(*s);
|
||||||
|
slice->setName(Strings::general_copy_of(slice->name()));
|
||||||
|
// Offset a bit the duplicated slice to avoid overlapping
|
||||||
|
offset(slice, 2, 2);
|
||||||
|
|
||||||
|
tx(new cmd::AddSlice(sprite, slice));
|
||||||
|
doc->notifySliceDuplicated(slice);
|
||||||
|
}
|
||||||
|
tx.commit();
|
||||||
|
|
||||||
|
std::string sliceName;
|
||||||
|
if (selectedSlices.size() == 1)
|
||||||
|
sliceName = selectedSlices[0]->name();
|
||||||
|
|
||||||
|
StatusBar::instance()->invalidate();
|
||||||
|
if (!sliceName.empty()) {
|
||||||
|
StatusBar::instance()->showTip(1000, Strings::duplicate_slice_x_duplicated(sliceName));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
StatusBar::instance()->showTip(
|
||||||
|
1000,
|
||||||
|
Strings::duplicate_slice_n_slices_duplicated(selectedSlices.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Command* CommandFactory::createDuplicateSliceCommand()
|
||||||
|
{
|
||||||
|
return new DuplicateSliceCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "app/commands/new_params.h"
|
#include "app/commands/new_params.h"
|
||||||
#include "app/commands/params.h"
|
#include "app/commands/params.h"
|
||||||
#include "app/context_access.h"
|
#include "app/context_access.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/ini_file.h"
|
#include "app/ini_file.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
|
|
@ -64,7 +65,8 @@ void DuplicateSpriteCommand::onExecute(Context* context)
|
||||||
|
|
||||||
std::string duplicateFn = params().filename.isSet() ?
|
std::string duplicateFn = params().filename.isSet() ?
|
||||||
params().filename() :
|
params().filename() :
|
||||||
base::get_file_title(fn) + " Copy" + (!ext.empty() ? "." + ext : "");
|
Strings::general_copy_of(base::get_file_title(fn)) +
|
||||||
|
(!ext.empty() ? "." + ext : "");
|
||||||
|
|
||||||
bool flatten = params().flatten.isSet() ? params().flatten() :
|
bool flatten = params().flatten.isSet() ? params().flatten() :
|
||||||
get_config_bool("DuplicateSprite", "Flatten", false);
|
get_config_bool("DuplicateSprite", "Flatten", false);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
#include "app/ui/app_menuitem.h"
|
#include "app/ui/app_menuitem.h"
|
||||||
#include "app/ui/keyboard_shortcuts.h"
|
#include "app/ui/keyboard_shortcuts.h"
|
||||||
#include "app/ui/search_entry.h"
|
#include "app/ui/search_entry.h"
|
||||||
#include "app/ui/select_accelerator.h"
|
#include "app/ui/select_shortcut.h"
|
||||||
#include "app/ui/separator_in_view.h"
|
#include "app/ui/separator_in_view.h"
|
||||||
#include "app/ui/skin/skin_theme.h"
|
#include "app/ui/skin/skin_theme.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
|
|
@ -151,7 +151,7 @@ public:
|
||||||
, m_keyOrig(key ? new Key(*key) : nullptr)
|
, m_keyOrig(key ? new Key(*key) : nullptr)
|
||||||
, m_menuitem(menuitem)
|
, m_menuitem(menuitem)
|
||||||
, m_level(level)
|
, m_level(level)
|
||||||
, m_hotAccel(-1)
|
, m_hotShortcut(-1)
|
||||||
, m_lockButtons(false)
|
, m_lockButtons(false)
|
||||||
, m_headerItem(headerItem)
|
, m_headerItem(headerItem)
|
||||||
{
|
{
|
||||||
|
|
@ -204,45 +204,45 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onChangeAccel(int index)
|
void onChangeShortcut(int index)
|
||||||
{
|
{
|
||||||
LockButtons lock(this);
|
LockButtons lock(this);
|
||||||
Accelerator origAccel = m_key->accels()[index];
|
Shortcut origShortcut = m_key->shortcuts()[index];
|
||||||
SelectAccelerator window(origAccel, m_key->keycontext(), m_keys);
|
SelectShortcut window(origShortcut, m_key->keycontext(), m_keys);
|
||||||
window.openWindowInForeground();
|
window.openWindowInForeground();
|
||||||
|
|
||||||
if (window.isModified()) {
|
if (window.isModified()) {
|
||||||
m_key->disableAccel(origAccel, KeySource::UserDefined);
|
m_key->disableShortcut(origShortcut, KeySource::UserDefined);
|
||||||
if (!window.accel().isEmpty())
|
if (!window.shortcut().isEmpty())
|
||||||
m_key->add(window.accel(), KeySource::UserDefined, m_keys);
|
m_key->add(window.shortcut(), KeySource::UserDefined, m_keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->window()->layout();
|
this->window()->layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDeleteAccel(int index)
|
void onDeleteShortcut(int index)
|
||||||
{
|
{
|
||||||
LockButtons lock(this);
|
LockButtons lock(this);
|
||||||
// We need to create a copy of the accelerator because
|
// We need to create a copy of the shortcut because
|
||||||
// Key::disableAccel() will modify the accels() collection itself.
|
// Key::disableShortcut() will modify the shortcuts() collection itself.
|
||||||
ui::Accelerator accel = m_key->accels()[index];
|
ui::Shortcut shortcut = m_key->shortcuts()[index];
|
||||||
|
|
||||||
if (ui::Alert::show(Strings::alerts_delete_shortcut(accel.toString())) != 1)
|
if (ui::Alert::show(Strings::alerts_delete_shortcut(shortcut.toString())) != 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_key->disableAccel(accel, KeySource::UserDefined);
|
m_key->disableShortcut(shortcut, KeySource::UserDefined);
|
||||||
window()->layout();
|
window()->layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAddAccel()
|
void onAddShortcut()
|
||||||
{
|
{
|
||||||
LockButtons lock(this);
|
LockButtons lock(this);
|
||||||
ui::Accelerator accel;
|
ui::Shortcut shortcut;
|
||||||
SelectAccelerator window(accel, m_key ? m_key->keycontext() : KeyContext::Any, m_keys);
|
SelectShortcut window(shortcut, m_key ? m_key->keycontext() : KeyContext::Any, m_keys);
|
||||||
window.openWindowInForeground();
|
window.openWindowInForeground();
|
||||||
|
|
||||||
if ((window.isModified()) ||
|
if ((window.isModified()) ||
|
||||||
// We can assign a "None" accelerator to mouse wheel actions
|
// We can assign a "None" shortcut to mouse wheel actions
|
||||||
(m_key && m_key->type() == KeyType::WheelAction && window.isOK())) {
|
(m_key && m_key->type() == KeyType::WheelAction && window.isOK())) {
|
||||||
if (!m_key) {
|
if (!m_key) {
|
||||||
ASSERT(m_menuitem);
|
ASSERT(m_menuitem);
|
||||||
|
|
@ -256,7 +256,7 @@ private:
|
||||||
m_menuKeys[m_menuitem] = m_key;
|
m_menuKeys[m_menuitem] = m_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_key->add(window.accel(), KeySource::UserDefined, m_keys);
|
m_key->add(window.shortcut(), KeySource::UserDefined, m_keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->window()->layout();
|
this->window()->layout();
|
||||||
|
|
@ -273,8 +273,8 @@ private:
|
||||||
size.w = std::max(size.w, w);
|
size.w = std::max(size.w, w);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_key && !m_key->accels().empty()) {
|
if (m_key && !m_key->shortcuts().empty()) {
|
||||||
size_t combos = m_key->accels().size();
|
size_t combos = m_key->shortcuts().size();
|
||||||
if (combos > 1)
|
if (combos > 1)
|
||||||
size.h *= combos;
|
size.h *= combos;
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +315,7 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_key && !m_key->accels().empty()) {
|
if (m_key && !m_key->shortcuts().empty()) {
|
||||||
if (m_key->keycontext() != KeyContext::Any) {
|
if (m_key->keycontext() != KeyContext::Any) {
|
||||||
g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()),
|
g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()),
|
||||||
fg,
|
fg,
|
||||||
|
|
@ -324,13 +324,14 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
const int dh = th + 4 * guiscale();
|
const int dh = th + 4 * guiscale();
|
||||||
IntersectClip clip(g,
|
IntersectClip clip(
|
||||||
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->accels().size()));
|
g,
|
||||||
|
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->shortcuts().size()));
|
||||||
if (clip) {
|
if (clip) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (const Accelerator& accel : m_key->accels()) {
|
for (const Shortcut& shortcut : m_key->shortcuts()) {
|
||||||
if (i != m_hotAccel || !m_changeButton) {
|
if (i != m_hotShortcut || !m_changeButton) {
|
||||||
g->drawText(getAccelText(accel), fg, bg, gfx::Point(keyXPos, y));
|
g->drawText(getShortcutText(shortcut), fg, bg, gfx::Point(keyXPos, y));
|
||||||
}
|
}
|
||||||
y += dh;
|
y += dh;
|
||||||
++i;
|
++i;
|
||||||
|
|
@ -361,40 +362,41 @@ private:
|
||||||
gfx::Rect bounds = this->bounds();
|
gfx::Rect bounds = this->bounds();
|
||||||
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
|
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
|
||||||
|
|
||||||
const Accelerators* accels = (m_key ? &m_key->accels() : NULL);
|
const Shortcuts* shortcuts = (m_key ? &m_key->shortcuts() : NULL);
|
||||||
int y = bounds.y;
|
int y = bounds.y;
|
||||||
int dh = textSize().h + 4 * guiscale();
|
int dh = textSize().h + 4 * guiscale();
|
||||||
int maxi = (accels && accels->size() > 1 ? accels->size() : 1);
|
int maxi = (shortcuts && shortcuts->size() > 1 ? shortcuts->size() : 1);
|
||||||
|
|
||||||
auto theme = SkinTheme::get(this);
|
auto* theme = SkinTheme::get(this);
|
||||||
|
|
||||||
for (int i = 0; i < maxi; ++i, y += dh) {
|
for (int i = 0; i < maxi; ++i, y += dh) {
|
||||||
int w = font()->textLength(
|
int w = font()->textLength((shortcuts && i < (int)shortcuts->size() ?
|
||||||
(accels && i < (int)accels->size() ? getAccelText((*accels)[i]) : std::string()));
|
getShortcutText((*shortcuts)[i]) :
|
||||||
|
std::string()));
|
||||||
gfx::Rect itemBounds(bounds.x + m_headerItem->keyXPos(), y, w, dh);
|
gfx::Rect itemBounds(bounds.x + m_headerItem->keyXPos(), y, w, dh);
|
||||||
itemBounds = itemBounds.enlarge(
|
itemBounds = itemBounds.enlarge(
|
||||||
gfx::Border(4 * guiscale(), 0, 6 * guiscale(), 1 * guiscale()));
|
gfx::Border(4 * guiscale(), 0, 6 * guiscale(), 1 * guiscale()));
|
||||||
|
|
||||||
if (accels && i < (int)accels->size() && mouseMsg->position().y >= itemBounds.y &&
|
if (shortcuts && i < (int)shortcuts->size() && mouseMsg->position().y >= itemBounds.y &&
|
||||||
mouseMsg->position().y < itemBounds.y + itemBounds.h) {
|
mouseMsg->position().y < itemBounds.y + itemBounds.h) {
|
||||||
if (m_hotAccel != i) {
|
if (m_hotShortcut != i) {
|
||||||
m_hotAccel = i;
|
m_hotShortcut = i;
|
||||||
|
|
||||||
m_changeConn = obs::connection();
|
m_changeConn = obs::connection();
|
||||||
m_changeButton.reset(new Button(""));
|
m_changeButton.reset(new Button(""));
|
||||||
m_changeConn = m_changeButton->Click.connect([this, i] { onChangeAccel(i); });
|
m_changeConn = m_changeButton->Click.connect([this, i] { onChangeShortcut(i); });
|
||||||
m_changeButton->setStyle(theme->styles.miniButton());
|
m_changeButton->setStyle(theme->styles.miniButton());
|
||||||
addChild(m_changeButton.get());
|
addChild(m_changeButton.get());
|
||||||
|
|
||||||
m_deleteConn = obs::connection();
|
m_deleteConn = obs::connection();
|
||||||
m_deleteButton.reset(new Button(""));
|
m_deleteButton.reset(new Button(""));
|
||||||
m_deleteConn = m_deleteButton->Click.connect([this, i] { onDeleteAccel(i); });
|
m_deleteConn = m_deleteButton->Click.connect([this, i] { onDeleteShortcut(i); });
|
||||||
m_deleteButton->setStyle(theme->styles.miniButton());
|
m_deleteButton->setStyle(theme->styles.miniButton());
|
||||||
addChild(m_deleteButton.get());
|
addChild(m_deleteButton.get());
|
||||||
|
|
||||||
m_changeButton->setBgColor(gfx::ColorNone);
|
m_changeButton->setBgColor(gfx::ColorNone);
|
||||||
m_changeButton->setBounds(itemBounds);
|
m_changeButton->setBounds(itemBounds);
|
||||||
m_changeButton->setText(getAccelText((*accels)[i]));
|
m_changeButton->setText(getShortcutText((*shortcuts)[i]));
|
||||||
|
|
||||||
const char* label = "x";
|
const char* label = "x";
|
||||||
m_deleteButton->setBgColor(gfx::ColorNone);
|
m_deleteButton->setBgColor(gfx::ColorNone);
|
||||||
|
|
@ -411,7 +413,7 @@ private:
|
||||||
if (i == 0 && !m_addButton && (!m_menuitem || m_menuitem->getCommand())) {
|
if (i == 0 && !m_addButton && (!m_menuitem || m_menuitem->getCommand())) {
|
||||||
m_addConn = obs::connection();
|
m_addConn = obs::connection();
|
||||||
m_addButton.reset(new Button(""));
|
m_addButton.reset(new Button(""));
|
||||||
m_addConn = m_addButton->Click.connect([this] { onAddAccel(); });
|
m_addConn = m_addButton->Click.connect([this] { onAddShortcut(); });
|
||||||
m_addButton->setStyle(theme->styles.miniButton());
|
m_addButton->setStyle(theme->styles.miniButton());
|
||||||
addChild(m_addButton.get());
|
addChild(m_addButton.get());
|
||||||
|
|
||||||
|
|
@ -452,17 +454,15 @@ private:
|
||||||
m_addButton->setVisible(false);
|
m_addButton->setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_hotAccel = -1;
|
m_hotShortcut = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string getAccelText(const Accelerator& accel) const
|
std::string getShortcutText(const Shortcut& shortcut) const
|
||||||
{
|
{
|
||||||
if (m_key && m_key->type() == KeyType::WheelAction && accel.isEmpty()) {
|
if (m_key && m_key->type() == KeyType::WheelAction && shortcut.isEmpty()) {
|
||||||
return Strings::keyboard_shortcuts_default_action();
|
return Strings::keyboard_shortcuts_default_action();
|
||||||
}
|
}
|
||||||
else {
|
return shortcut.toString();
|
||||||
return accel.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardShortcuts& m_keys;
|
KeyboardShortcuts& m_keys;
|
||||||
|
|
@ -471,14 +471,14 @@ private:
|
||||||
KeyPtr m_keyOrig;
|
KeyPtr m_keyOrig;
|
||||||
AppMenuItem* m_menuitem;
|
AppMenuItem* m_menuitem;
|
||||||
int m_level;
|
int m_level;
|
||||||
ui::Accelerators m_newAccels;
|
ui::Shortcuts m_newShortcuts;
|
||||||
std::shared_ptr<ui::Button> m_changeButton;
|
std::shared_ptr<ui::Button> m_changeButton;
|
||||||
std::shared_ptr<ui::Button> m_deleteButton;
|
std::shared_ptr<ui::Button> m_deleteButton;
|
||||||
std::shared_ptr<ui::Button> m_addButton;
|
std::shared_ptr<ui::Button> m_addButton;
|
||||||
obs::scoped_connection m_changeConn;
|
obs::scoped_connection m_changeConn;
|
||||||
obs::scoped_connection m_deleteConn;
|
obs::scoped_connection m_deleteConn;
|
||||||
obs::scoped_connection m_addConn;
|
obs::scoped_connection m_addConn;
|
||||||
int m_hotAccel;
|
int m_hotShortcut;
|
||||||
bool m_lockButtons;
|
bool m_lockButtons;
|
||||||
HeaderItem* m_headerItem;
|
HeaderItem* m_headerItem;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2024 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -135,8 +135,16 @@ public:
|
||||||
|
|
||||||
remapWindow();
|
remapWindow();
|
||||||
centerWindow();
|
centerWindow();
|
||||||
|
|
||||||
|
gfx::Rect originalBounds = bounds();
|
||||||
|
|
||||||
load_window_pos(this, "LayerProperties");
|
load_window_pos(this, "LayerProperties");
|
||||||
|
|
||||||
|
// Queue a remap for after the user data view is configured
|
||||||
|
// if the window size has been reset and user data is visible
|
||||||
|
if (originalBounds == bounds() && Preferences::instance().layers.userDataVisibility())
|
||||||
|
m_remapAfterConfigure = true;
|
||||||
|
|
||||||
UIContext::instance()->add_observer(this);
|
UIContext::instance()->add_observer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +166,12 @@ public:
|
||||||
m_document->add_observer(this);
|
m_document->add_observer(this);
|
||||||
|
|
||||||
if (countLayers() > 0) {
|
if (countLayers() > 0) {
|
||||||
m_userDataView.configureAndSet(m_layer->userData(), g_window->propertiesGrid());
|
m_userDataView.configureAndSet(m_layer->userData(), propertiesGrid());
|
||||||
|
if (m_remapAfterConfigure) {
|
||||||
|
remapWindow();
|
||||||
|
centerWindow();
|
||||||
|
m_remapAfterConfigure = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromLayer();
|
updateFromLayer();
|
||||||
|
|
@ -355,8 +368,7 @@ private:
|
||||||
{
|
{
|
||||||
if (m_layer) {
|
if (m_layer) {
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
g_window->remapWindow();
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
manager()->invalidate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,6 +496,7 @@ private:
|
||||||
view::RealRange m_range;
|
view::RealRange m_range;
|
||||||
bool m_selfUpdate = false;
|
bool m_selfUpdate = false;
|
||||||
UserDataView m_userDataView;
|
UserDataView m_userDataView;
|
||||||
|
bool m_remapAfterConfigure = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
LayerPropertiesCommand::LayerPropertiesCommand()
|
LayerPropertiesCommand::LayerPropertiesCommand()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -164,10 +164,10 @@ void NewBrushCommand::createBrush(const Site& site, const Mask* mask)
|
||||||
params.set("change", "custom");
|
params.set("change", "custom");
|
||||||
params.set("slot", base::convert_to<std::string>(slot).c_str());
|
params.set("slot", base::convert_to<std::string>(slot).c_str());
|
||||||
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
|
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
|
||||||
if (key && !key->accels().empty()) {
|
if (key && !key->shortcuts().empty()) {
|
||||||
std::string tooltip;
|
std::string tooltip;
|
||||||
tooltip += Strings::new_brush_shortcut() + " ";
|
tooltip += Strings::new_brush_shortcut() + " ";
|
||||||
tooltip += key->accels().front().toString();
|
tooltip += key->shortcuts().front().toString();
|
||||||
StatusBar::instance()->showTip(2000, tooltip);
|
StatusBar::instance()->showTip(2000, tooltip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -696,7 +696,6 @@ public:
|
||||||
|
|
||||||
onChangeBgScope();
|
onChangeBgScope();
|
||||||
onChangeGridScope();
|
onChangeGridScope();
|
||||||
sectionListbox()->selectIndex(m_curSection);
|
|
||||||
|
|
||||||
// Aseprite format preferences
|
// Aseprite format preferences
|
||||||
celFormat()->setSelectedItemIndex(int(m_pref.asepriteFormat.celFormat()));
|
celFormat()->setSelectedItemIndex(int(m_pref.asepriteFormat.celFormat()));
|
||||||
|
|
@ -1046,6 +1045,13 @@ public:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onOpen(Event& evt) override
|
||||||
|
{
|
||||||
|
sectionListbox()->selectIndex(m_curSection);
|
||||||
|
app::gen::Options::onOpen(evt);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onInitTheme(InitThemeEvent& ev) override
|
void onInitTheme(InitThemeEvent& ev) override
|
||||||
{
|
{
|
||||||
|
|
@ -1366,8 +1372,8 @@ private:
|
||||||
if (!item)
|
if (!item)
|
||||||
return;
|
return;
|
||||||
const std::string lang = item->langId();
|
const std::string lang = item->langId();
|
||||||
const bool state = (lang == "ar" || lang == "ja" || lang == "ko" || lang == "yue_Hant" ||
|
const bool state = (lang == "ar" || lang == "ja" || lang == "ko" || lang == "th" ||
|
||||||
lang == "zh_Hans" || lang == "zh_Hant");
|
lang == "yue_Hant" || lang == "zh_Hans" || lang == "zh_Hant");
|
||||||
fontWarningFiller()->setVisible(state);
|
fontWarningFiller()->setVisible(state);
|
||||||
fontWarning()->setVisible(state);
|
fontWarning()->setVisible(state);
|
||||||
layout();
|
layout();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "app/app.h"
|
#include "app/app.h"
|
||||||
|
#include "app/color_utils.h"
|
||||||
#include "app/commands/command.h"
|
#include "app/commands/command.h"
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
#include "app/context.h"
|
#include "app/context.h"
|
||||||
|
|
@ -88,10 +89,10 @@ void PasteTextCommand::onExecute(Context* ctx)
|
||||||
std::string text = window.userText()->text();
|
std::string text = window.userText()->text();
|
||||||
app::Color color = window.fontColor()->getColor();
|
app::Color color = window.fontColor()->getColor();
|
||||||
|
|
||||||
doc::ImageRef image = render_text(
|
ui::Paint paint = window.fontFace()->paint();
|
||||||
fontInfo,
|
paint.color(color_utils::color_for_ui(color));
|
||||||
text,
|
|
||||||
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
|
doc::ImageRef image = render_text(fontInfo, text, paint);
|
||||||
if (image) {
|
if (image) {
|
||||||
Sprite* sprite = editor->sprite();
|
Sprite* sprite = editor->sprite();
|
||||||
if (image->pixelFormat() != sprite->pixelFormat()) {
|
if (image->pixelFormat() != sprite->pixelFormat()) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -188,8 +188,7 @@ private:
|
||||||
void onToggleUserData()
|
void onToggleUserData()
|
||||||
{
|
{
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
remapWindow();
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
manager()->invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTilesedDuplicated(const Tileset* tilesetClone)
|
void onTilesedDuplicated(const Tileset* tilesetClone)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#include "app/doc_undo.h"
|
#include "app/doc_undo.h"
|
||||||
#include "app/doc_undo_observer.h"
|
#include "app/doc_undo_observer.h"
|
||||||
#include "app/docs_observer.h"
|
#include "app/docs_observer.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/modules/gui.h"
|
#include "app/modules/gui.h"
|
||||||
#include "app/modules/palettes.h"
|
#include "app/modules/palettes.h"
|
||||||
#include "app/site.h"
|
#include "app/site.h"
|
||||||
|
|
@ -292,7 +293,7 @@ public:
|
||||||
base::get_pretty_memory_size(static_cast<Cmd*>(state->cmd())->memSize())
|
base::get_pretty_memory_size(static_cast<Cmd*>(state->cmd())->memSize())
|
||||||
#endif
|
#endif
|
||||||
:
|
:
|
||||||
std::string("Initial State"));
|
Strings::undo_history_initial_state());
|
||||||
|
|
||||||
if ((g->getClipBounds() & itemBounds).isEmpty())
|
if ((g->getClipBounds() & itemBounds).isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2023 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
FOR_EACH_COMMAND(About)
|
FOR_EACH_COMMAND(About)
|
||||||
FOR_EACH_COMMAND(AddColor)
|
FOR_EACH_COMMAND(AddColor)
|
||||||
FOR_EACH_COMMAND(AdvancedMode)
|
FOR_EACH_COMMAND(AdvancedMode)
|
||||||
|
FOR_EACH_COMMAND(Apply)
|
||||||
FOR_EACH_COMMAND(AutocropSprite)
|
FOR_EACH_COMMAND(AutocropSprite)
|
||||||
FOR_EACH_COMMAND(BackgroundFromLayer)
|
FOR_EACH_COMMAND(BackgroundFromLayer)
|
||||||
FOR_EACH_COMMAND(BrightnessContrast)
|
FOR_EACH_COMMAND(BrightnessContrast)
|
||||||
|
|
@ -40,6 +41,7 @@ FOR_EACH_COMMAND(DeselectMask)
|
||||||
FOR_EACH_COMMAND(Despeckle)
|
FOR_EACH_COMMAND(Despeckle)
|
||||||
FOR_EACH_COMMAND(DiscardBrush)
|
FOR_EACH_COMMAND(DiscardBrush)
|
||||||
FOR_EACH_COMMAND(DuplicateLayer)
|
FOR_EACH_COMMAND(DuplicateLayer)
|
||||||
|
FOR_EACH_COMMAND(DuplicateSlice)
|
||||||
FOR_EACH_COMMAND(DuplicateSprite)
|
FOR_EACH_COMMAND(DuplicateSprite)
|
||||||
FOR_EACH_COMMAND(DuplicateView)
|
FOR_EACH_COMMAND(DuplicateView)
|
||||||
FOR_EACH_COMMAND(Exit)
|
FOR_EACH_COMMAND(Exit)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -128,7 +128,7 @@ private:
|
||||||
case ui::kKeyDownMessage: {
|
case ui::kKeyDownMessage: {
|
||||||
KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
||||||
const KeyPtr key = keys->command(CommandId::SwitchColors());
|
const KeyPtr key = keys->command(CommandId::SwitchColors());
|
||||||
if (key && key->isPressed(msg, *keys)) {
|
if (key && key->isPressed(msg)) {
|
||||||
// Switch colors
|
// Switch colors
|
||||||
app::Color from = m_fromButton->getColor();
|
app::Color from = m_fromButton->getColor();
|
||||||
app::Color to = m_toButton->getColor();
|
app::Color to = m_toButton->getColor();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -48,9 +48,12 @@ public:
|
||||||
|
|
||||||
// When the main window is closed, we should close the console (in
|
// When the main window is closed, we should close the console (in
|
||||||
// other case the main message loop will continue running for the
|
// other case the main message loop will continue running for the
|
||||||
// console too).
|
// console too). The main window can be nullptr if the console is
|
||||||
m_mainWindowClosedConn = App::instance()->mainWindow()->Close.connect(
|
// used to show an error when loading the default theme or font at
|
||||||
[this] { closeWindow(nullptr); });
|
// the initialization.
|
||||||
|
if (auto* mainWin = App::instance()->mainWindow()) {
|
||||||
|
m_mainWindowClosedConn = mainWin->Close.connect([this] { closeWindow(nullptr); });
|
||||||
|
}
|
||||||
|
|
||||||
// When the window is closed, we clear the text
|
// When the window is closed, we clear the text
|
||||||
Close.connect([this] {
|
Close.connect([this] {
|
||||||
|
|
@ -78,7 +81,7 @@ public:
|
||||||
|
|
||||||
~ConsoleWindow() { TRACE_CON("CON: ~ConsoleWindow this=", this); }
|
~ConsoleWindow() { TRACE_CON("CON: ~ConsoleWindow this=", this); }
|
||||||
|
|
||||||
void addMessage(const std::string& msg)
|
void addMessage(std::string msg)
|
||||||
{
|
{
|
||||||
if (!m_hasText) {
|
if (!m_hasText) {
|
||||||
m_hasText = true;
|
m_hasText = true;
|
||||||
|
|
@ -90,6 +93,17 @@ public:
|
||||||
gfx::Point pt = m_view.viewScroll();
|
gfx::Point pt = m_view.viewScroll();
|
||||||
const bool autoScroll = (pt.y >= maxSize.h - visible.h);
|
const bool autoScroll = (pt.y >= maxSize.h - visible.h);
|
||||||
|
|
||||||
|
// Escape characters we can't show properly
|
||||||
|
for (size_t i = 0; i < msg.size(); i++) {
|
||||||
|
switch (msg[i]) {
|
||||||
|
case '\a':
|
||||||
|
case '\b':
|
||||||
|
case '\r':
|
||||||
|
case '\t':
|
||||||
|
case '\v': msg[i] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_textbox.setText(m_textbox.text() + msg);
|
m_textbox.setText(m_textbox.text() + msg);
|
||||||
|
|
||||||
if (autoScroll) {
|
if (autoScroll) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2023 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -139,6 +139,13 @@ DataRecovery::Sessions DataRecovery::sessions()
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DataRecovery::isRunningSession(const SessionPtr& session) const
|
||||||
|
{
|
||||||
|
ASSERT(session);
|
||||||
|
ASSERT(m_inProgress);
|
||||||
|
return session->path() == m_inProgress->path();
|
||||||
|
}
|
||||||
|
|
||||||
void DataRecovery::searchForSessions()
|
void DataRecovery::searchForSessions()
|
||||||
{
|
{
|
||||||
Sessions sessions;
|
Sessions sessions;
|
||||||
|
|
@ -150,7 +157,7 @@ void DataRecovery::searchForSessions()
|
||||||
RECO_TRACE("RECO: Session '%s' ", itempath.c_str());
|
RECO_TRACE("RECO: Session '%s' ", itempath.c_str());
|
||||||
|
|
||||||
SessionPtr session(new Session(&m_config, itempath));
|
SessionPtr session(new Session(&m_config, itempath));
|
||||||
if (!session->isRunning()) {
|
if (!isRunningSession(session)) {
|
||||||
if ((session->isEmpty()) || (!session->isCrashedSession() && session->isOldSession())) {
|
if ((session->isEmpty()) || (!session->isCrashedSession() && session->isOldSession())) {
|
||||||
RECO_TRACE("to be deleted (%s)\n",
|
RECO_TRACE("to be deleted (%s)\n",
|
||||||
session->isEmpty() ? "is empty" :
|
session->isEmpty() ? "is empty" :
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2021 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -45,6 +45,8 @@ public:
|
||||||
// Returns a copy of the list of sessions that can be recovered.
|
// Returns a copy of the list of sessions that can be recovered.
|
||||||
Sessions sessions();
|
Sessions sessions();
|
||||||
|
|
||||||
|
bool isRunningSession(const SessionPtr& session) const;
|
||||||
|
|
||||||
// Triggered in the UI-thread from the m_thread using an
|
// Triggered in the UI-thread from the m_thread using an
|
||||||
// ui::execute_from_ui_thread() when the list of sessions is ready
|
// ui::execute_from_ui_thread() when the list of sessions is ready
|
||||||
// to be used.
|
// to be used.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -19,12 +19,10 @@
|
||||||
#include "app/crash/write_document.h"
|
#include "app/crash/write_document.h"
|
||||||
#include "app/doc.h"
|
#include "app/doc.h"
|
||||||
#include "app/doc_access.h"
|
#include "app/doc_access.h"
|
||||||
#include "app/file/file.h"
|
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "base/convert_to.h"
|
#include "base/convert_to.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
#include "base/fstream_path.h"
|
#include "base/fstream_path.h"
|
||||||
#include "base/process.h"
|
|
||||||
#include "base/split_string.h"
|
#include "base/split_string.h"
|
||||||
#include "base/string.h"
|
#include "base/string.h"
|
||||||
#include "base/thread.h"
|
#include "base/thread.h"
|
||||||
|
|
@ -128,12 +126,6 @@ const Session::Backups& Session::backups()
|
||||||
return m_backups;
|
return m_backups;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::isRunning()
|
|
||||||
{
|
|
||||||
loadPid();
|
|
||||||
return base::get_process_name(m_pid) == base::get_process_name(base::get_current_process_id());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Session::isCrashedSession()
|
bool Session::isCrashedSession()
|
||||||
{
|
{
|
||||||
loadPid();
|
loadPid();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -47,9 +47,9 @@ public:
|
||||||
|
|
||||||
std::string name() const;
|
std::string name() const;
|
||||||
std::string version();
|
std::string version();
|
||||||
|
std::string& path() { return m_path; }
|
||||||
const Backups& backups();
|
const Backups& backups();
|
||||||
|
|
||||||
bool isRunning();
|
|
||||||
bool isCrashedSession();
|
bool isCrashedSession();
|
||||||
bool isOldSession();
|
bool isOldSession();
|
||||||
bool isEmpty();
|
bool isEmpty();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -338,6 +338,19 @@ void Doc::notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti)
|
||||||
notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev);
|
notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Doc::notifyBeforeSlicesDuplication()
|
||||||
|
{
|
||||||
|
DocEvent ev(this);
|
||||||
|
notify_observers<DocEvent&>(&DocObserver::onBeforeSlicesDuplication, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Doc::notifySliceDuplicated(Slice* slice)
|
||||||
|
{
|
||||||
|
DocEvent ev(this);
|
||||||
|
ev.slice(slice);
|
||||||
|
notify_observers<DocEvent&>(&DocObserver::onSliceDuplicated, ev);
|
||||||
|
}
|
||||||
|
|
||||||
bool Doc::isModified() const
|
bool Doc::isModified() const
|
||||||
{
|
{
|
||||||
return !m_undo->isInSavedStateOrSimilar();
|
return !m_undo->isInSavedStateOrSimilar();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -141,6 +141,8 @@ public:
|
||||||
void notifyTilesetChanged(Tileset* tileset);
|
void notifyTilesetChanged(Tileset* tileset);
|
||||||
void notifyLayerGroupCollapseChange(Layer* layer);
|
void notifyLayerGroupCollapseChange(Layer* layer);
|
||||||
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
|
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
|
||||||
|
void notifyBeforeSlicesDuplication();
|
||||||
|
void notifySliceDuplicated(Slice* slice);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// File related properties
|
// File related properties
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include "app/cmd/add_cel.h"
|
#include "app/cmd/add_cel.h"
|
||||||
#include "app/cmd/add_frame.h"
|
#include "app/cmd/add_frame.h"
|
||||||
#include "app/cmd/add_layer.h"
|
#include "app/cmd/add_layer.h"
|
||||||
|
#include "app/cmd/add_tileset.h"
|
||||||
#include "app/cmd/clear_cel.h"
|
#include "app/cmd/clear_cel.h"
|
||||||
#include "app/cmd/clear_image.h"
|
#include "app/cmd/clear_image.h"
|
||||||
#include "app/cmd/copy_cel.h"
|
#include "app/cmd/copy_cel.h"
|
||||||
|
|
@ -44,6 +45,7 @@
|
||||||
#include "app/context.h"
|
#include "app/context.h"
|
||||||
#include "app/doc.h"
|
#include "app/doc.h"
|
||||||
#include "app/doc_undo.h"
|
#include "app/doc_undo.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/snap_to_grid.h"
|
#include "app/snap_to_grid.h"
|
||||||
#include "app/transaction.h"
|
#include "app/transaction.h"
|
||||||
|
|
@ -692,12 +694,44 @@ void DocApi::restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeT
|
||||||
restackLayerAfter(layer, parent, afterThis);
|
restackLayerAfter(layer, parent, afterThis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layer* DocApi::copyLayerWithSprite(doc::Layer* layer, doc::Sprite* sprite)
|
||||||
|
{
|
||||||
|
std::unique_ptr<doc::Layer> clone;
|
||||||
|
if (layer->isTilemap()) {
|
||||||
|
auto* srcTilemap = static_cast<LayerTilemap*>(layer);
|
||||||
|
tileset_index tilesetIndex = srcTilemap->tilesetIndex();
|
||||||
|
// If the caller is trying to make a copy of a tilemap layer specifying a
|
||||||
|
// different sprite as its owner, then we must copy the tilesets of the
|
||||||
|
// given tilemap layer into the new owner.
|
||||||
|
if (sprite != srcTilemap->sprite()) {
|
||||||
|
auto* srcTilesetCopy = Tileset::MakeCopyCopyingImagesForSprite(srcTilemap->tileset(), sprite);
|
||||||
|
auto* addTileset = new cmd::AddTileset(sprite, srcTilesetCopy);
|
||||||
|
m_transaction.execute(addTileset);
|
||||||
|
tilesetIndex = addTileset->tilesetIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
clone = std::make_unique<LayerTilemap>(sprite, tilesetIndex);
|
||||||
|
}
|
||||||
|
else if (layer->isImage())
|
||||||
|
clone = std::make_unique<LayerImage>(sprite);
|
||||||
|
else if (layer->isGroup())
|
||||||
|
clone = std::make_unique<LayerGroup>(sprite);
|
||||||
|
else
|
||||||
|
throw std::runtime_error("Invalid layer type");
|
||||||
|
|
||||||
|
if (auto* doc = dynamic_cast<app::Doc*>(sprite->document())) {
|
||||||
|
doc->copyLayerContent(layer, doc, clone.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone.release();
|
||||||
|
}
|
||||||
|
|
||||||
Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer* afterLayer)
|
Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer* afterLayer)
|
||||||
{
|
{
|
||||||
ASSERT(parent);
|
ASSERT(parent);
|
||||||
Layer* newLayerPtr = copy_layer(sourceLayer);
|
Layer* newLayerPtr = copyLayerWithSprite(sourceLayer, parent->sprite());
|
||||||
|
|
||||||
newLayerPtr->setName(newLayerPtr->name() + " Copy");
|
newLayerPtr->setName(Strings::general_copy_of(newLayerPtr->name()));
|
||||||
|
|
||||||
addLayer(parent, newLayerPtr, afterLayer);
|
addLayer(parent, newLayerPtr, afterLayer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#define APP_DOC_API_H_INCLUDED
|
#define APP_DOC_API_H_INCLUDED
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "app/doc_api_dnd_helper.h"
|
||||||
#include "app/drop_frame_place.h"
|
#include "app/drop_frame_place.h"
|
||||||
#include "app/tags_handling.h"
|
#include "app/tags_handling.h"
|
||||||
#include "doc/algorithm/flip_type.h"
|
#include "doc/algorithm/flip_type.h"
|
||||||
|
|
@ -38,6 +39,7 @@ class Doc;
|
||||||
class Transaction;
|
class Transaction;
|
||||||
|
|
||||||
using namespace doc;
|
using namespace doc;
|
||||||
|
using namespace docapi;
|
||||||
|
|
||||||
// High-level API to modify a document adding undo information, i.e.
|
// High-level API to modify a document adding undo information, i.e.
|
||||||
// adding new "Cmd"s in the given transaction.
|
// adding new "Cmd"s in the given transaction.
|
||||||
|
|
@ -134,6 +136,14 @@ public:
|
||||||
// Palette API
|
// Palette API
|
||||||
void setPalette(Sprite* sprite, frame_t frame, const Palette* newPalette);
|
void setPalette(Sprite* sprite, frame_t frame, const Palette* newPalette);
|
||||||
|
|
||||||
|
// Drag and Drop helper API
|
||||||
|
void dropDocumentsOnTimeline(app::Doc* doc,
|
||||||
|
doc::frame_t frame,
|
||||||
|
doc::layer_t layerIndex,
|
||||||
|
InsertionPoint insert,
|
||||||
|
DroppedOn droppedOn,
|
||||||
|
DocProvider& provider);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void cropImageLayer(LayerImage* layer, const gfx::Rect& bounds, const bool trimOutside);
|
void cropImageLayer(LayerImage* layer, const gfx::Rect& bounds, const bool trimOutside);
|
||||||
bool cropCel(LayerImage* layer, Cel* cel, const gfx::Rect& bounds, const bool trimOutside);
|
bool cropCel(LayerImage* layer, Cel* cel, const gfx::Rect& bounds, const bool trimOutside);
|
||||||
|
|
@ -145,6 +155,8 @@ private:
|
||||||
const DropFramePlace dropFramePlace,
|
const DropFramePlace dropFramePlace,
|
||||||
const TagsHandling tagsHandling);
|
const TagsHandling tagsHandling);
|
||||||
|
|
||||||
|
Layer* copyLayerWithSprite(doc::Layer* layer, doc::Sprite* sprite);
|
||||||
|
|
||||||
class HandleLinkedCels {
|
class HandleLinkedCels {
|
||||||
public:
|
public:
|
||||||
HandleLinkedCels(DocApi& api,
|
HandleLinkedCels(DocApi& api,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "app/doc_api.h"
|
||||||
|
|
||||||
|
#include "app/cmd/copy_cel.h"
|
||||||
|
#include "app/cmd/set_pixel_format.h"
|
||||||
|
#include "app/cmd/set_total_frames.h"
|
||||||
|
#include "app/file/file.h"
|
||||||
|
#include "app/transaction.h"
|
||||||
|
#include "doc/layer.h"
|
||||||
|
#include "render/dithering.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace app;
|
||||||
|
|
||||||
|
// Returns true if the document srcDoc has a cel that can be copied into a
|
||||||
|
// destDoc's cel.
|
||||||
|
// The cel from srcDoc can be copied only when all of the following conditions
|
||||||
|
// are met:
|
||||||
|
// * Drop took place in a cel.
|
||||||
|
// * Source doc has only one layer with just one frame.
|
||||||
|
// * The layer from the source doc and the destination cel's layer are both
|
||||||
|
// Image layers.
|
||||||
|
// Otherwise this function returns false.
|
||||||
|
bool can_copy_cel(Doc* srcDoc, Doc* destDoc, DroppedOn droppedOn, doc::layer_t layerIndex)
|
||||||
|
{
|
||||||
|
auto* srcLayer = srcDoc->sprite()->firstLayer();
|
||||||
|
auto* destLayer = destDoc->sprite()->allLayers()[layerIndex];
|
||||||
|
return droppedOn == DroppedOn::Cel && srcDoc->sprite()->allLayersCount() == 1 &&
|
||||||
|
srcDoc->sprite()->totalFrames() == 1 && srcLayer->isImage() && destLayer->isImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_insertion_layer(Doc* destDoc,
|
||||||
|
doc::layer_t layerIndex,
|
||||||
|
InsertionPoint& insert,
|
||||||
|
Layer*& layer,
|
||||||
|
LayerGroup*& group)
|
||||||
|
{
|
||||||
|
const LayerList& allLayers = destDoc->sprite()->allLayers();
|
||||||
|
layer = allLayers[layerIndex];
|
||||||
|
if (insert == InsertionPoint::BeforeLayer && layer->isGroup()) {
|
||||||
|
group = static_cast<LayerGroup*>(layer);
|
||||||
|
// The user is trying to drop layers into an empty group, so there is no after
|
||||||
|
// nor before layer...
|
||||||
|
if (group->layersCount() == 0) {
|
||||||
|
layer = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer = group->lastLayer();
|
||||||
|
insert = InsertionPoint::AfterLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
group = layer->parent();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
using namespace docapi;
|
||||||
|
|
||||||
|
void DocApi::dropDocumentsOnTimeline(app::Doc* destDoc,
|
||||||
|
doc::frame_t frame,
|
||||||
|
doc::layer_t layerIndex,
|
||||||
|
InsertionPoint insert,
|
||||||
|
DroppedOn droppedOn,
|
||||||
|
DocProvider& docProvider)
|
||||||
|
{
|
||||||
|
// Layer used as a reference to determine if the dropped layers will be
|
||||||
|
// inserted after or before it.
|
||||||
|
Layer* refLayer = nullptr;
|
||||||
|
// Parent group of the reference layer layer.
|
||||||
|
LayerGroup* group = nullptr;
|
||||||
|
// Keep track of the current insertion point.
|
||||||
|
setup_insertion_layer(destDoc, layerIndex, insert, refLayer, group);
|
||||||
|
|
||||||
|
int docsProcessed = 0;
|
||||||
|
while (docProvider.pendingDocs() > 0) {
|
||||||
|
std::unique_ptr<Doc> srcDoc = docProvider.nextDoc();
|
||||||
|
docsProcessed++;
|
||||||
|
|
||||||
|
// If the provider returned a null document then there was some problem with
|
||||||
|
// that specific doc but it can continue providing more documents
|
||||||
|
if (!srcDoc)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// If source document doesn't match the destination document's color
|
||||||
|
// mode, change it.
|
||||||
|
if (srcDoc->colorMode() != destDoc->colorMode()) {
|
||||||
|
// We can change the source doc pixel format out of any transaction because
|
||||||
|
// we don't need undo/redo it.
|
||||||
|
cmd::SetPixelFormat cmd(srcDoc->sprite(),
|
||||||
|
destDoc->sprite()->pixelFormat(),
|
||||||
|
render::Dithering(),
|
||||||
|
Preferences::instance().quantization.rgbmapAlgorithm(),
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
FitCriteria::DEFAULT);
|
||||||
|
cmd.execute(destDoc->context());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is only one source document to process and it has a single cel
|
||||||
|
// that can be copied, then copy the cel from the source doc into the
|
||||||
|
// destination doc's selected frame.
|
||||||
|
const bool isJustOneDoc = (docsProcessed == 1 && docProvider.pendingDocs() == 0);
|
||||||
|
if (isJustOneDoc && can_copy_cel(srcDoc.get(), destDoc, droppedOn, layerIndex)) {
|
||||||
|
auto* srcLayer = static_cast<LayerImage*>(srcDoc->sprite()->firstLayer());
|
||||||
|
auto* destLayer = static_cast<LayerImage*>(destDoc->sprite()->allLayers()[layerIndex]);
|
||||||
|
m_transaction.execute(new cmd::CopyCel(srcLayer, 0, destLayer, frame, false));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no room for the source frames, add frames to the
|
||||||
|
// destination sprite.
|
||||||
|
if (frame + srcDoc->sprite()->totalFrames() > destDoc->sprite()->totalFrames()) {
|
||||||
|
m_transaction.execute(
|
||||||
|
new cmd::SetTotalFrames(destDoc->sprite(), frame + srcDoc->sprite()->totalFrames()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto* srcLayer : srcDoc->sprite()->root()->layers()) {
|
||||||
|
srcLayer->displaceFrames(0, frame);
|
||||||
|
if (insert == InsertionPoint::AfterLayer) {
|
||||||
|
refLayer = duplicateLayerAfter(srcLayer, group, refLayer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
refLayer = duplicateLayerBefore(srcLayer, group, refLayer);
|
||||||
|
insert = InsertionPoint::AfterLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#ifndef APP_DOC_API_DND_HELPER_H_INCLUDED
|
||||||
|
#define APP_DOC_API_DND_HELPER_H_INCLUDED
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app/doc.h"
|
||||||
|
|
||||||
|
namespace app { namespace docapi {
|
||||||
|
|
||||||
|
enum class InsertionPoint {
|
||||||
|
BeforeLayer,
|
||||||
|
AfterLayer,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DroppedOn {
|
||||||
|
Unspecified,
|
||||||
|
Frame,
|
||||||
|
Layer,
|
||||||
|
Cel,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Used by the drag & drop helper API as a provider of the documents that
|
||||||
|
// were dragged and then dropped in some Drag&Drop aware widget.
|
||||||
|
class DocProvider {
|
||||||
|
public:
|
||||||
|
virtual ~DocProvider() = default;
|
||||||
|
// Returns the next document from the underlying provider's implementation,
|
||||||
|
// returns null if there is no more documents to provide.
|
||||||
|
virtual std::unique_ptr<Doc> nextDoc() = 0;
|
||||||
|
// Returns the number of docs that were not provided yet.
|
||||||
|
virtual int pendingDocs() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace app::docapi
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
#include "gfx/size.h"
|
#include "gfx/size.h"
|
||||||
#include "render/dithering.h"
|
#include "render/dithering.h"
|
||||||
#include "render/ordered_dither.h"
|
#include "render/ordered_dither.h"
|
||||||
|
#include "render/quantization.h"
|
||||||
#include "render/render.h"
|
#include "render/render.h"
|
||||||
#include "ver/info.h"
|
#include "ver/info.h"
|
||||||
|
|
||||||
|
|
@ -69,6 +70,104 @@ std::string escape_for_json(const std::string& path)
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
void serialize_properties(const doc::UserData::Properties& props, std::ostream& os);
|
||||||
|
|
||||||
|
// Helper for a single value
|
||||||
|
void serialize_variant(const doc::UserData::Variant& value, std::ostream& os)
|
||||||
|
{
|
||||||
|
using Properties = doc::UserData::Properties;
|
||||||
|
switch (value.index()) {
|
||||||
|
case USER_DATA_PROPERTY_TYPE_BOOL: os << (get_value<bool>(value) ? "true" : "false"); break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_INT8: os << static_cast<int64_t>(get_value<int8_t>(value)); break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_UINT8:
|
||||||
|
os << static_cast<int64_t>(get_value<uint8_t>(value));
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_INT16:
|
||||||
|
os << static_cast<int64_t>(get_value<int16_t>(value));
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_UINT16:
|
||||||
|
os << static_cast<int64_t>(get_value<uint16_t>(value));
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_INT32:
|
||||||
|
os << static_cast<int64_t>(get_value<int32_t>(value));
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_UINT32:
|
||||||
|
os << static_cast<int64_t>(get_value<uint32_t>(value));
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_INT64:
|
||||||
|
os << static_cast<int64_t>(get_value<int64_t>(value));
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_UINT64:
|
||||||
|
os << static_cast<int64_t>(get_value<uint64_t>(value));
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_FLOAT: os << get_value<float>(value); break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_DOUBLE: os << get_value<double>(value); break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_STRING:
|
||||||
|
os << "\"" << escape_for_json(get_value<std::string>(value)) << "\"";
|
||||||
|
break;
|
||||||
|
case USER_DATA_PROPERTY_TYPE_PROPERTIES:
|
||||||
|
serialize_properties(get_value<Properties>(value), os);
|
||||||
|
break;
|
||||||
|
default: os << "\"[unsupported type]\""; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serializes a map of properties
|
||||||
|
void serialize_properties(const doc::UserData::Properties& props, std::ostream& os)
|
||||||
|
{
|
||||||
|
os << "{";
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& [key, value] : props) {
|
||||||
|
if (!first)
|
||||||
|
os << ", ";
|
||||||
|
first = false;
|
||||||
|
os << "\"" << escape_for_json(key) << "\": ";
|
||||||
|
serialize_variant(value, os);
|
||||||
|
}
|
||||||
|
os << "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
void serialize_userdata_properties(const doc::UserData& data, std::ostream& os)
|
||||||
|
{
|
||||||
|
const auto& propsMaps = data.propertiesMaps();
|
||||||
|
bool hasAnyProps = false;
|
||||||
|
for (const auto& [group, props] : propsMaps) {
|
||||||
|
if (!props.empty()) {
|
||||||
|
hasAnyProps = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasAnyProps) {
|
||||||
|
os << ", \"properties\": {";
|
||||||
|
bool firstProp = true;
|
||||||
|
for (const auto& [group, props] : propsMaps) {
|
||||||
|
if (!props.empty()) {
|
||||||
|
if (!firstProp)
|
||||||
|
os << ", ";
|
||||||
|
firstProp = false;
|
||||||
|
if (group.empty()) {
|
||||||
|
// Default group: flatten its keys at the top level
|
||||||
|
bool firstKey = true;
|
||||||
|
for (const auto& [key, value] : props) {
|
||||||
|
if (!firstKey)
|
||||||
|
os << ", ";
|
||||||
|
firstKey = false;
|
||||||
|
os << "\"" << escape_for_json(key) << "\": ";
|
||||||
|
serialize_variant(value, os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Named group: nest under its group name
|
||||||
|
os << "\"" << escape_for_json(group) << "\": ";
|
||||||
|
serialize_properties(props, os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
|
std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
|
||||||
{
|
{
|
||||||
doc::color_t color = data.color();
|
doc::color_t color = data.color();
|
||||||
|
|
@ -80,6 +179,9 @@ std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
|
||||||
}
|
}
|
||||||
if (!data.text().empty())
|
if (!data.text().empty())
|
||||||
os << ", \"data\": \"" << escape_for_json(data.text()) << "\"";
|
os << ", \"data\": \"" << escape_for_json(data.text()) << "\"";
|
||||||
|
|
||||||
|
serialize_userdata_properties(data, os);
|
||||||
|
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,6 +402,35 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPixelFormat(const doc::PixelFormat newPixelFormat)
|
||||||
|
{
|
||||||
|
if (!m_image || m_image->pixelFormat() == newPixelFormat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImageSpec spec(ColorMode(newPixelFormat),
|
||||||
|
m_image->width(),
|
||||||
|
m_image->height(),
|
||||||
|
m_image->maskColor());
|
||||||
|
ImageRef convertedImg(Image::create(spec));
|
||||||
|
if (!convertedImg)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clear_image(convertedImg.get(), 0);
|
||||||
|
render::Dithering dithering;
|
||||||
|
Sprite* sprite = this->sprite();
|
||||||
|
render::convert_pixel_format(m_image.get(),
|
||||||
|
convertedImg.get(),
|
||||||
|
newPixelFormat,
|
||||||
|
dithering,
|
||||||
|
sprite ? sprite->rgbMap(0) : nullptr,
|
||||||
|
sprite ? sprite->palette(0) : nullptr,
|
||||||
|
(sprite && sprite->backgroundLayer()),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
nullptr);
|
||||||
|
m_image = convertedImg;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Doc* m_document;
|
Doc* m_document;
|
||||||
Sprite* m_sprite;
|
Sprite* m_sprite;
|
||||||
|
|
@ -679,6 +810,9 @@ Doc* DocExporter::exportSheet(Context* ctx, base::task_token& token)
|
||||||
Sprite* texture = textureDocument->sprite();
|
Sprite* texture = textureDocument->sprite();
|
||||||
Image* textureImage = texture->root()->firstLayer()->cel(frame_t(0))->image();
|
Image* textureImage = texture->root()->firstLayer()->cel(frame_t(0))->image();
|
||||||
|
|
||||||
|
for (auto& sample : samples)
|
||||||
|
sample.setPixelFormat(texture->pixelFormat());
|
||||||
|
|
||||||
renderTexture(ctx, samples, textureImage, token);
|
renderTexture(ctx, samples, textureImage, token);
|
||||||
if (token.canceled())
|
if (token.canceled())
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -1245,22 +1379,6 @@ void DocExporter::renderTexture(Context* ctx,
|
||||||
++i;
|
++i;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the sprite compatible with the texture so the render()
|
|
||||||
// works correctly.
|
|
||||||
if (sample.sprite()->pixelFormat() != textureImage->pixelFormat()) {
|
|
||||||
RgbMapAlgorithm rgbmapAlgo = Preferences::instance().quantization.rgbmapAlgorithm();
|
|
||||||
FitCriteria fc = Preferences::instance().quantization.fitCriteria();
|
|
||||||
cmd::SetPixelFormat(sample.sprite(),
|
|
||||||
textureImage->pixelFormat(),
|
|
||||||
render::Dithering(),
|
|
||||||
rgbmapAlgo,
|
|
||||||
nullptr, // toGray is not needed because the texture is Indexed or RGB
|
|
||||||
nullptr, // TODO add a delegate to show progress
|
|
||||||
fc)
|
|
||||||
.execute(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
sample.renderSample(textureImage,
|
sample.renderSample(textureImage,
|
||||||
sample.inTextureBounds().x + m_innerPadding,
|
sample.inTextureBounds().x + m_innerPadding,
|
||||||
sample.inTextureBounds().y + m_innerPadding,
|
sample.inTextureBounds().y + m_innerPadding,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -90,6 +90,8 @@ public:
|
||||||
|
|
||||||
// Slices
|
// Slices
|
||||||
virtual void onSliceNameChange(DocEvent& ev) {}
|
virtual void onSliceNameChange(DocEvent& ev) {}
|
||||||
|
virtual void onBeforeSlicesDuplication(DocEvent& ev) {}
|
||||||
|
virtual void onSliceDuplicated(DocEvent& ev) {}
|
||||||
|
|
||||||
// The tileset has changed.
|
// The tileset has changed.
|
||||||
virtual void onTilesetChanged(DocEvent& ev) {}
|
virtual void onTilesetChanged(DocEvent& ev) {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2020 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2023 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -1095,6 +1095,9 @@ public:
|
||||||
gifframe_t nframes = totalFrames();
|
gifframe_t nframes = totalFrames();
|
||||||
for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) {
|
for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) {
|
||||||
ASSERT(frame_it != frame_end);
|
ASSERT(frame_it != frame_end);
|
||||||
|
if (m_fop->isStop())
|
||||||
|
break;
|
||||||
|
|
||||||
frame_t frame = *frame_it;
|
frame_t frame = *frame_it;
|
||||||
++frame_it;
|
++frame_it;
|
||||||
|
|
||||||
|
|
@ -1401,7 +1404,7 @@ private:
|
||||||
color_t color = *srcIt;
|
color_t color = *srcIt;
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if (rgba_geta(color) >= 128) {
|
if (rgba_geta(color) > 0) {
|
||||||
i = framePalette.findExactMatch(rgba_getr(color),
|
i = framePalette.findExactMatch(rgba_getr(color),
|
||||||
rgba_getg(color),
|
rgba_getg(color),
|
||||||
rgba_getb(color),
|
rgba_getb(color),
|
||||||
|
|
|
||||||
|
|
@ -75,13 +75,15 @@ bool SvgFormat::onSave(FileOp* fop)
|
||||||
const int pixelScaleValue = std::clamp(svg_options->pixelScale, 0, 10000);
|
const int pixelScaleValue = std::clamp(svg_options->pixelScale, 0, 10000);
|
||||||
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
|
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
|
||||||
FILE* f = handle.get();
|
FILE* f = handle.get();
|
||||||
auto printcol = [f](int x, int y, int r, int g, int b, int a, int pxScale) {
|
|
||||||
|
auto printRect =
|
||||||
|
[f](int x, int y, int width, int height, int r, int g, int b, int a, int pxScale) {
|
||||||
fprintf(f,
|
fprintf(f,
|
||||||
"<rect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"#%02X%02X%02X\" ",
|
"<rect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"#%02X%02X%02X\" ",
|
||||||
x * pxScale,
|
x * pxScale,
|
||||||
y * pxScale,
|
y * pxScale,
|
||||||
pxScale,
|
width * pxScale,
|
||||||
pxScale,
|
height * pxScale,
|
||||||
r,
|
r,
|
||||||
g,
|
g,
|
||||||
b);
|
b);
|
||||||
|
|
@ -89,6 +91,70 @@ bool SvgFormat::onSave(FileOp* fop)
|
||||||
fprintf(f, "opacity=\"%f\" ", (float)a / 255.0);
|
fprintf(f, "opacity=\"%f\" ", (float)a / 255.0);
|
||||||
fprintf(f, "/>\n");
|
fprintf(f, "/>\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function for greedy merge of same-colored pixels
|
||||||
|
auto mergePixels = [&](auto getPixelColor) {
|
||||||
|
std::vector<std::vector<bool>> processed(image->height(),
|
||||||
|
std::vector<bool>(image->width(), false));
|
||||||
|
|
||||||
|
for (y = 0; y < image->height(); y++) {
|
||||||
|
for (x = 0; x < image->width(); x++) {
|
||||||
|
if (processed[y][x])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto [pixelR, pixelG, pixelB, pixelA, shouldSkip] = getPixelColor(x, y);
|
||||||
|
if (shouldSkip) {
|
||||||
|
processed[y][x] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the largest rectangle starting from this pixel
|
||||||
|
int maxWidth = 0;
|
||||||
|
int maxHeight = 0;
|
||||||
|
|
||||||
|
// Find maximum width for current row
|
||||||
|
for (int w = x; w < image->width(); w++) {
|
||||||
|
if (processed[y][w])
|
||||||
|
break;
|
||||||
|
auto [checkR, checkG, checkB, checkA, skip] = getPixelColor(w, y);
|
||||||
|
if (skip || checkR != pixelR || checkG != pixelG || checkB != pixelB || checkA != pixelA)
|
||||||
|
break;
|
||||||
|
maxWidth = w - x + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find maximum height with current width
|
||||||
|
for (int h = y; h < image->height(); h++) {
|
||||||
|
bool canExtend = true;
|
||||||
|
for (int w = x; w < x + maxWidth; w++) {
|
||||||
|
if (processed[h][w]) {
|
||||||
|
canExtend = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto [checkR, checkG, checkB, checkA, skip] = getPixelColor(w, h);
|
||||||
|
if (skip || checkR != pixelR || checkG != pixelG || checkB != pixelB ||
|
||||||
|
checkA != pixelA) {
|
||||||
|
canExtend = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!canExtend)
|
||||||
|
break;
|
||||||
|
maxHeight = h - y + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark rectangle as processed and output it
|
||||||
|
for (int h = y; h < y + maxHeight; h++) {
|
||||||
|
for (int w = x; w < x + maxWidth; w++) {
|
||||||
|
processed[h][w] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printRect(x, y, maxWidth, maxHeight, pixelR, pixelG, pixelB, pixelA, pixelScaleValue);
|
||||||
|
}
|
||||||
|
fop->setProgress((float)y / (float)(image->height()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fprintf(f, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
|
fprintf(f, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
|
||||||
fprintf(
|
fprintf(
|
||||||
f,
|
f,
|
||||||
|
|
@ -98,59 +164,55 @@ bool SvgFormat::onSave(FileOp* fop)
|
||||||
|
|
||||||
switch (image->pixelFormat()) {
|
switch (image->pixelFormat()) {
|
||||||
case IMAGE_RGB: {
|
case IMAGE_RGB: {
|
||||||
for (y = 0; y < image->height(); y++) {
|
auto getPixelColor = [&](int px, int py) -> std::tuple<int, int, int, int, bool> {
|
||||||
for (x = 0; x < image->width(); x++) {
|
c = get_pixel_fast<RgbTraits>(image.get(), px, py);
|
||||||
c = get_pixel_fast<RgbTraits>(image.get(), x, y);
|
|
||||||
alpha = rgba_geta(c);
|
alpha = rgba_geta(c);
|
||||||
if (alpha != 0x00)
|
if (alpha == 0x00)
|
||||||
printcol(x, y, rgba_getr(c), rgba_getg(c), rgba_getb(c), alpha, pixelScaleValue);
|
return { 0, 0, 0, 0, true };
|
||||||
}
|
return { rgba_getr(c), rgba_getg(c), rgba_getb(c), alpha, false };
|
||||||
fop->setProgress((float)y / (float)(image->height()));
|
};
|
||||||
}
|
mergePixels(getPixelColor);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case IMAGE_GRAYSCALE: {
|
case IMAGE_GRAYSCALE: {
|
||||||
for (y = 0; y < image->height(); y++) {
|
auto getPixelColor = [&](int px, int py) -> std::tuple<int, int, int, int, bool> {
|
||||||
for (x = 0; x < image->width(); x++) {
|
c = get_pixel_fast<GrayscaleTraits>(image.get(), px, py);
|
||||||
c = get_pixel_fast<GrayscaleTraits>(image.get(), x, y);
|
|
||||||
auto v = graya_getv(c);
|
auto v = graya_getv(c);
|
||||||
alpha = graya_geta(c);
|
alpha = graya_geta(c);
|
||||||
if (alpha != 0x00)
|
if (alpha == 0x00)
|
||||||
printcol(x, y, v, v, v, alpha, pixelScaleValue);
|
return { 0, 0, 0, 0, true };
|
||||||
}
|
return { v, v, v, alpha, false };
|
||||||
fop->setProgress((float)y / (float)(image->height()));
|
};
|
||||||
}
|
mergePixels(getPixelColor);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case IMAGE_INDEXED: {
|
case IMAGE_INDEXED: {
|
||||||
unsigned char image_palette[256][4];
|
unsigned char image_palette[256][4];
|
||||||
for (y = 0; y < 256; y++) {
|
for (int i = 0; i < 256; i++) {
|
||||||
fop->sequenceGetColor(y, &r, &g, &b);
|
fop->sequenceGetColor(i, &r, &g, &b);
|
||||||
image_palette[y][0] = r;
|
image_palette[i][0] = r;
|
||||||
image_palette[y][1] = g;
|
image_palette[i][1] = g;
|
||||||
image_palette[y][2] = b;
|
image_palette[i][2] = b;
|
||||||
fop->sequenceGetAlpha(y, &a);
|
fop->sequenceGetAlpha(i, &a);
|
||||||
image_palette[y][3] = a;
|
image_palette[i][3] = a;
|
||||||
}
|
}
|
||||||
color_t mask_color = -1;
|
color_t mask_color = -1;
|
||||||
if (fop->document()->sprite()->backgroundLayer() == NULL ||
|
if (fop->document()->sprite()->backgroundLayer() == NULL ||
|
||||||
!fop->document()->sprite()->backgroundLayer()->isVisible()) {
|
!fop->document()->sprite()->backgroundLayer()->isVisible()) {
|
||||||
mask_color = fop->document()->sprite()->transparentColor();
|
mask_color = fop->document()->sprite()->transparentColor();
|
||||||
}
|
}
|
||||||
for (y = 0; y < image->height(); y++) {
|
|
||||||
for (x = 0; x < image->width(); x++) {
|
auto getPixelColor = [&](int px, int py) -> std::tuple<int, int, int, int, bool> {
|
||||||
c = get_pixel_fast<IndexedTraits>(image.get(), x, y);
|
c = get_pixel_fast<IndexedTraits>(image.get(), px, py);
|
||||||
if (c != mask_color)
|
if (c == mask_color)
|
||||||
printcol(x,
|
return { 0, 0, 0, 0, true };
|
||||||
y,
|
return { image_palette[c][0] & 0xff,
|
||||||
image_palette[c][0] & 0xff,
|
|
||||||
image_palette[c][1] & 0xff,
|
image_palette[c][1] & 0xff,
|
||||||
image_palette[c][2] & 0xff,
|
image_palette[c][2] & 0xff,
|
||||||
image_palette[c][3] & 0xff,
|
image_palette[c][3] & 0xff,
|
||||||
pixelScaleValue);
|
false };
|
||||||
}
|
};
|
||||||
fop->setProgress((float)y / (float)(image->height()));
|
mergePixels(getPixelColor);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -82,6 +82,9 @@ public:
|
||||||
unsigned int m_version;
|
unsigned int m_version;
|
||||||
bool m_removed;
|
bool m_removed;
|
||||||
mutable bool m_is_folder;
|
mutable bool m_is_folder;
|
||||||
|
#ifdef _WIN32
|
||||||
|
bool m_isHidden = false;
|
||||||
|
#endif
|
||||||
std::atomic<double> m_thumbnailProgress;
|
std::atomic<double> m_thumbnailProgress;
|
||||||
std::atomic<os::Surface*> m_thumbnail;
|
std::atomic<os::Surface*> m_thumbnail;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
@ -266,7 +269,7 @@ IFileItem* FileSystemModule::getRootFileItem()
|
||||||
fileitem->m_pidl = pidl;
|
fileitem->m_pidl = pidl;
|
||||||
fileitem->m_fullpidl = pidl;
|
fileitem->m_fullpidl = pidl;
|
||||||
|
|
||||||
SFGAOF attrib = SFGAO_FOLDER;
|
SFGAOF attrib = SFGAO_FOLDER | SFGAO_HIDDEN;
|
||||||
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
|
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
|
||||||
|
|
||||||
update_by_pidl(fileitem, attrib);
|
update_by_pidl(fileitem, attrib);
|
||||||
|
|
@ -357,7 +360,7 @@ bool FileItem::isHidden() const
|
||||||
ASSERT(m_displayname != NOTINITIALIZED);
|
ASSERT(m_displayname != NOTINITIALIZED);
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return false;
|
return m_isHidden;
|
||||||
#else
|
#else
|
||||||
return m_displayname[0] == '.';
|
return m_displayname[0] == '.';
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -462,7 +465,7 @@ const FileItemList& FileItem::children()
|
||||||
// Get the interface to enumerate subitems
|
// Get the interface to enumerate subitems
|
||||||
hr = pFolder->EnumObjects(
|
hr = pFolder->EnumObjects(
|
||||||
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
|
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
|
||||||
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
|
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN,
|
||||||
&pEnum);
|
&pEnum);
|
||||||
|
|
||||||
if (hr == S_OK && pEnum) {
|
if (hr == S_OK && pEnum) {
|
||||||
|
|
@ -473,10 +476,9 @@ const FileItemList& FileItem::children()
|
||||||
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
|
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
|
||||||
// Request the SFGAO_FOLDER attribute to know what of the
|
// Request the SFGAO_FOLDER attribute to know what of the
|
||||||
// item is file or a folder
|
// item is file or a folder
|
||||||
for (c = 0; c < fetched; ++c) {
|
for (c = 0; c < fetched; ++c)
|
||||||
attribs[c] = SFGAO_FOLDER;
|
attribs[c] = SFGAO_FOLDER | SFGAO_HIDDEN;
|
||||||
pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)itempidl, attribs + c);
|
pFolder->GetAttributesOf(fetched, (LPCITEMIDLIST*)itempidl, attribs);
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the FileItems
|
// Generate the FileItems
|
||||||
for (c = 0; c < fetched; ++c) {
|
for (c = 0; c < fetched; ++c) {
|
||||||
|
|
@ -755,6 +757,9 @@ static void update_by_pidl(FileItem* fileitem, SFGAOF attrib)
|
||||||
// Is it a folder?
|
// Is it a folder?
|
||||||
|
|
||||||
fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);
|
fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);
|
||||||
|
#if _WIN32
|
||||||
|
fileitem->m_isHidden = (attrib & SFGAO_HIDDEN ? true : false);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Get the name to display
|
// Get the name to display
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@
|
||||||
#include "text/font.h"
|
#include "text/font.h"
|
||||||
#include "text/font_mgr.h"
|
#include "text/font_mgr.h"
|
||||||
#include "text/sprite_sheet_font.h"
|
#include "text/sprite_sheet_font.h"
|
||||||
|
#include "text/typeface.h"
|
||||||
#include "ui/manager.h"
|
#include "ui/manager.h"
|
||||||
|
#include "ui/scale.h"
|
||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
|
|
@ -87,9 +89,9 @@ text::FontRef FontData::getFont(text::FontMgrRef& fontMgr, float size)
|
||||||
|
|
||||||
// Load fallback
|
// Load fallback
|
||||||
if (m_fallback) {
|
if (m_fallback) {
|
||||||
text::FontRef fallback = m_fallback->getFont(fontMgr, m_fallbackSize);
|
text::FontRef fallback = m_fallback->getFont(fontMgr, m_fallbackSize * ui::guiscale());
|
||||||
if (font)
|
if (font)
|
||||||
font->setFallback(fallback.get());
|
font->setFallback(fallback);
|
||||||
else
|
else
|
||||||
return fallback; // Don't double-cache the fallback font
|
return fallback; // Don't double-cache the fallback font
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ public:
|
||||||
void setName(const std::string& name) { m_name = name; }
|
void setName(const std::string& name) { m_name = name; }
|
||||||
void setFilename(const std::string& filename) { m_filename = filename; }
|
void setFilename(const std::string& filename) { m_filename = filename; }
|
||||||
void setDefaultSize(const float size) { m_size = size; }
|
void setDefaultSize(const float size) { m_size = size; }
|
||||||
void setAntialias(bool antialias) { m_antialias = antialias; }
|
void setAntialias(const bool antialias) { m_antialias = antialias; }
|
||||||
|
void setHinting(const text::FontHinting hinting) { m_hinting = hinting; }
|
||||||
void setFallback(FontData* fallback, float fallbackSize)
|
void setFallback(FontData* fallback, float fallbackSize)
|
||||||
{
|
{
|
||||||
m_fallback = fallback;
|
m_fallback = fallback;
|
||||||
|
|
|
||||||
|
|
@ -681,12 +681,25 @@ void CustomizedGuiManager::onNewDisplayConfiguration(Display* display)
|
||||||
bool CustomizedGuiManager::processKey(Message* msg)
|
bool CustomizedGuiManager::processKey(Message* msg)
|
||||||
{
|
{
|
||||||
App* app = App::instance();
|
App* app = App::instance();
|
||||||
|
const KeyContext currentCtx = KeyboardShortcuts::getCurrentKeyContext();
|
||||||
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
||||||
const KeyContext contexts[] = { keys->getCurrentKeyContext(), KeyContext::Normal };
|
const KeyContext contexts[] = { currentCtx, KeyContext::Normal };
|
||||||
int n = (contexts[0] != contexts[1] ? 2 : 1);
|
int n = (contexts[0] != contexts[1] ? 2 : 1);
|
||||||
|
|
||||||
|
// Find best match (prefer the shortcut that matches the context first)
|
||||||
|
KeyPtr key = nullptr;
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
for (const KeyPtr& key : *keys) {
|
for (const KeyPtr& k : *keys) {
|
||||||
if (key->isPressed(msg, *keys, contexts[i])) {
|
if (k->isPressed(msg, contexts[i]) &&
|
||||||
|
(!key ||
|
||||||
|
(key->keycontext() != currentCtx && match_key_context(k->keycontext(), currentCtx)))) {
|
||||||
|
key = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!key)
|
||||||
|
return false;
|
||||||
|
|
||||||
// Cancel menu-bar loops (to close any popup menu)
|
// Cancel menu-bar loops (to close any popup menu)
|
||||||
app->mainWindow()->getMenuBar()->cancelMenuLoop();
|
app->mainWindow()->getMenuBar()->cancelMenuLoop();
|
||||||
|
|
||||||
|
|
@ -700,7 +713,7 @@ bool CustomizedGuiManager::processKey(Message* msg)
|
||||||
// Collect all tools with the pressed keyboard-shortcut
|
// Collect all tools with the pressed keyboard-shortcut
|
||||||
for (tools::Tool* tool : *toolbox) {
|
for (tools::Tool* tool : *toolbox) {
|
||||||
const KeyPtr key = keys->tool(tool);
|
const KeyPtr key = keys->tool(tool);
|
||||||
if (key && key->isPressed(msg, *keys))
|
if (key && key->isPressed(msg))
|
||||||
possibles.push_back(tool);
|
possibles.push_back(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -708,8 +721,7 @@ bool CustomizedGuiManager::processKey(Message* msg)
|
||||||
bool done = false;
|
bool done = false;
|
||||||
|
|
||||||
for (size_t i = 0; i < possibles.size(); ++i) {
|
for (size_t i = 0; i < possibles.size(); ++i) {
|
||||||
if (possibles[i] != current_tool &&
|
if (possibles[i] != current_tool && ToolBar::instance()->isToolVisible(possibles[i])) {
|
||||||
ToolBar::instance()->isToolVisible(possibles[i])) {
|
|
||||||
select_this_tool = possibles[i];
|
select_this_tool = possibles[i];
|
||||||
done = true;
|
done = true;
|
||||||
break;
|
break;
|
||||||
|
|
@ -752,10 +764,7 @@ bool CustomizedGuiManager::processKey(Message* msg)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ void ResourceFinder::includeDesktopDir(const char* filename)
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
std::vector<wchar_t> buf(MAX_PATH);
|
std::vector<wchar_t> buf(MAX_PATH);
|
||||||
HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOPDIRECTORY, NULL, SHGFP_TYPE_DEFAULT, &buf[0]);
|
HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOP, NULL, SHGFP_TYPE_CURRENT, &buf[0]);
|
||||||
if (hr == S_OK) {
|
if (hr == S_OK) {
|
||||||
addPath(base::join_path(base::to_utf8(&buf[0]), filename));
|
addPath(base::join_path(base::to_utf8(&buf[0]), filename));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@
|
||||||
|
|
||||||
// Increment this value if the scripting API is modified between two
|
// Increment this value if the scripting API is modified between two
|
||||||
// released Aseprite versions.
|
// released Aseprite versions.
|
||||||
#define API_VERSION 32
|
#define API_VERSION 35
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,10 @@
|
||||||
#include "app/tools/tool_loop_manager.h"
|
#include "app/tools/tool_loop_manager.h"
|
||||||
#include "app/tx.h"
|
#include "app/tx.h"
|
||||||
#include "app/ui/context_bar.h"
|
#include "app/ui/context_bar.h"
|
||||||
#include "app/ui/doc_view.h"
|
|
||||||
#include "app/ui/editor/editor.h"
|
#include "app/ui/editor/editor.h"
|
||||||
#include "app/ui/editor/tool_loop_impl.h"
|
#include "app/ui/editor/tool_loop_impl.h"
|
||||||
#include "app/ui/main_window.h"
|
#include "app/ui/main_window.h"
|
||||||
|
#include "app/ui/status_bar.h"
|
||||||
#include "app/ui/timeline/timeline.h"
|
#include "app/ui/timeline/timeline.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
|
|
@ -498,6 +498,44 @@ int App_useTool(lua_State* L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int App_tip(lua_State* L)
|
||||||
|
{
|
||||||
|
const auto* ctx = App::instance()->context();
|
||||||
|
if (!ctx || !ctx->isUIAvailable() || !StatusBar::instance())
|
||||||
|
return 0; // No UI to show the tooltip
|
||||||
|
|
||||||
|
std::string text;
|
||||||
|
double duration = 2.0;
|
||||||
|
|
||||||
|
if (lua_istable(L, 1)) {
|
||||||
|
int type = lua_getfield(L, 1, "text");
|
||||||
|
if (type == LUA_TSTRING)
|
||||||
|
text = lua_tostring(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 1, "duration");
|
||||||
|
if (type == LUA_TNUMBER)
|
||||||
|
duration = lua_tonumber(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!lua_isstring(L, 1))
|
||||||
|
return luaL_error(L, "app.tip text parameter must be a string");
|
||||||
|
|
||||||
|
text = lua_tostring(L, 1);
|
||||||
|
|
||||||
|
if (lua_isnumber(L, 2))
|
||||||
|
duration = lua_tonumber(L, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.empty())
|
||||||
|
return luaL_error(L, "app.tip text cannot be empty");
|
||||||
|
|
||||||
|
int msecs = std::clamp<int>(duration * 1000.0, 500, 30000);
|
||||||
|
StatusBar::instance()->showTip(msecs, text);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int App_get_events(lua_State* L)
|
int App_get_events(lua_State* L)
|
||||||
{
|
{
|
||||||
push_app_events(L);
|
push_app_events(L);
|
||||||
|
|
@ -820,6 +858,7 @@ const luaL_Reg App_methods[] = {
|
||||||
{ "alert", App_alert },
|
{ "alert", App_alert },
|
||||||
{ "refresh", App_refresh },
|
{ "refresh", App_refresh },
|
||||||
{ "useTool", App_useTool },
|
{ "useTool", App_useTool },
|
||||||
|
{ "tip", App_tip },
|
||||||
{ nullptr, nullptr }
|
{ nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2018 David Capello
|
// Copyright (C) 2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -60,6 +60,8 @@ namespace app { namespace script {
|
||||||
|
|
||||||
using namespace ui;
|
using namespace ui;
|
||||||
|
|
||||||
|
static constexpr const int kDefaultAutofit = ui::LEFT | ui::TOP;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class DialogWindow : public WindowWithHand {
|
class DialogWindow : public WindowWithHand {
|
||||||
|
|
@ -107,6 +109,7 @@ struct Dialog {
|
||||||
std::map<std::string, ui::Widget*> dataWidgets;
|
std::map<std::string, ui::Widget*> dataWidgets;
|
||||||
std::map<std::string, ui::Widget*> labelWidgets;
|
std::map<std::string, ui::Widget*> labelWidgets;
|
||||||
int currentRadioGroup = 0;
|
int currentRadioGroup = 0;
|
||||||
|
int autofit = kDefaultAutofit;
|
||||||
|
|
||||||
// Member used to hold current state about the creation of a tabs
|
// Member used to hold current state about the creation of a tabs
|
||||||
// widget. After creation it is reset to null to be ready for the
|
// widget. After creation it is reset to null to be ready for the
|
||||||
|
|
@ -127,12 +130,13 @@ struct Dialog {
|
||||||
int showRef = LUA_REFNIL;
|
int showRef = LUA_REFNIL;
|
||||||
lua_State* L = nullptr;
|
lua_State* L = nullptr;
|
||||||
|
|
||||||
Dialog(const ui::Window::Type windowType, const std::string& title)
|
Dialog(const ui::Window::Type windowType, const std::string& title, bool sizeable)
|
||||||
: window(windowType, title)
|
: window(windowType, title)
|
||||||
, grid(2, false)
|
, grid(2, false)
|
||||||
, currentGrid(&grid)
|
, currentGrid(&grid)
|
||||||
{
|
{
|
||||||
window.addChild(&grid);
|
window.addChild(&grid);
|
||||||
|
window.setSizeable(sizeable);
|
||||||
all_dialogs.push_back(this);
|
all_dialogs.push_back(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,11 +196,19 @@ struct Dialog {
|
||||||
it->second->setText(text);
|
it->second->setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAutofit(int align)
|
||||||
|
{
|
||||||
|
// Accept both 0 or a valid subset of align parameters.
|
||||||
|
if (align == 0 || (align & (ui::LEFT | ui::RIGHT | ui::TOP | ui::BOTTOM)))
|
||||||
|
autofit = align;
|
||||||
|
}
|
||||||
|
|
||||||
Display* parentDisplay() const
|
Display* parentDisplay() const
|
||||||
{
|
{
|
||||||
Display* parentDisplay = window.parentDisplay();
|
Display* parentDisplay = window.parentDisplay();
|
||||||
if (!parentDisplay) {
|
if (!parentDisplay) {
|
||||||
const auto mainWindow = App::instance()->mainWindow();
|
const auto* mainWindow = App::instance()->mainWindow();
|
||||||
|
if (mainWindow)
|
||||||
parentDisplay = mainWindow->display();
|
parentDisplay = mainWindow->display();
|
||||||
}
|
}
|
||||||
return parentDisplay;
|
return parentDisplay;
|
||||||
|
|
@ -209,6 +221,9 @@ struct Dialog {
|
||||||
// origin/scale (or main window if a parent window wasn't specified).
|
// origin/scale (or main window if a parent window wasn't specified).
|
||||||
if (window.ownDisplay()) {
|
if (window.ownDisplay()) {
|
||||||
const Display* parentDisplay = this->parentDisplay();
|
const Display* parentDisplay = this->parentDisplay();
|
||||||
|
if (!parentDisplay)
|
||||||
|
return bounds;
|
||||||
|
|
||||||
const int scale = parentDisplay->scale();
|
const int scale = parentDisplay->scale();
|
||||||
const gfx::Point dialogOrigin = window.display()->nativeWindow()->contentRect().origin();
|
const gfx::Point dialogOrigin = window.display()->nativeWindow()->contentRect().origin();
|
||||||
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
||||||
|
|
@ -223,6 +238,9 @@ struct Dialog {
|
||||||
window.expandWindow(rc.size());
|
window.expandWindow(rc.size());
|
||||||
|
|
||||||
Display* parentDisplay = this->parentDisplay();
|
Display* parentDisplay = this->parentDisplay();
|
||||||
|
if (!parentDisplay)
|
||||||
|
return;
|
||||||
|
|
||||||
const int scale = parentDisplay->scale();
|
const int scale = parentDisplay->scale();
|
||||||
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
||||||
gfx::Rect frame = window.display()->nativeWindow()->contentRect();
|
gfx::Rect frame = window.display()->nativeWindow()->contentRect();
|
||||||
|
|
@ -230,8 +248,10 @@ struct Dialog {
|
||||||
window.display()->nativeWindow()->setFrame(frame);
|
window.display()->nativeWindow()->setFrame(frame);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
gfx::Rect oldBounds(window.bounds());
|
||||||
window.setBounds(rc);
|
window.setBounds(rc);
|
||||||
window.invalidate();
|
window.invalidate();
|
||||||
|
parentDisplay()->invalidateRect(oldBounds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,6 +385,8 @@ int Dialog_new(lua_State* L)
|
||||||
// Get the title and the type of window (with or without title bar)
|
// Get the title and the type of window (with or without title bar)
|
||||||
ui::Window::Type windowType = ui::Window::WithTitleBar;
|
ui::Window::Type windowType = ui::Window::WithTitleBar;
|
||||||
std::string title = "Script";
|
std::string title = "Script";
|
||||||
|
bool sizeable = true;
|
||||||
|
int autofit = kDefaultAutofit;
|
||||||
if (lua_isstring(L, 1)) {
|
if (lua_isstring(L, 1)) {
|
||||||
title = lua_tostring(L, 1);
|
title = lua_tostring(L, 1);
|
||||||
}
|
}
|
||||||
|
|
@ -378,9 +400,21 @@ int Dialog_new(lua_State* L)
|
||||||
if (type != LUA_TNIL && lua_toboolean(L, -1))
|
if (type != LUA_TNIL && lua_toboolean(L, -1))
|
||||||
windowType = ui::Window::WithoutTitleBar;
|
windowType = ui::Window::WithoutTitleBar;
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 1, "resizeable");
|
||||||
|
if (type != LUA_TNIL && !lua_toboolean(L, -1))
|
||||||
|
sizeable = false;
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 1, "autofit");
|
||||||
|
if (type != LUA_TNIL) {
|
||||||
|
autofit = lua_tointeger(L, -1);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto dlg = push_new<Dialog>(L, windowType, title);
|
auto dlg = push_new<Dialog>(L, windowType, title, sizeable);
|
||||||
|
dlg->setAutofit(autofit);
|
||||||
|
|
||||||
// The uservalue of the dialog userdata will contain a table that
|
// The uservalue of the dialog userdata will contain a table that
|
||||||
// stores all the callbacks to handle events. As these callbacks can
|
// stores all the callbacks to handle events. As these callbacks can
|
||||||
|
|
@ -575,8 +609,9 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
|
||||||
bool vexpand = (widget->type() == Canvas::Type());
|
bool vexpand = (widget->type() == Canvas::Type());
|
||||||
|
|
||||||
// This is to separate different kind of widgets without label in
|
// This is to separate different kind of widgets without label in
|
||||||
// different rows.
|
// different rows. Separator widgets will always create a new row.
|
||||||
if (dlg->lastWidgetType != widget->type() || dlg->autoNewRow) {
|
if (dlg->lastWidgetType != widget->type() || dlg->autoNewRow ||
|
||||||
|
widget->type() == ui::kSeparatorWidget) {
|
||||||
dlg->lastWidgetType = widget->type();
|
dlg->lastWidgetType = widget->type();
|
||||||
dlg->hbox = nullptr;
|
dlg->hbox = nullptr;
|
||||||
}
|
}
|
||||||
|
|
@ -631,8 +666,8 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
|
||||||
dlg->labelWidgets[id] = labelWidget;
|
dlg->labelWidgets[id] = labelWidget;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// For tabs we don't want the empty space of an unspecified label.
|
// For tabs and separators, we don't want the empty space of an unspecified label.
|
||||||
if (widget->type() != Tabs::Type()) {
|
if (widget->type() != Tabs::Type() && widget->type() != ui::kSeparatorWidget) {
|
||||||
dlg->currentGrid->addChildInCell(new ui::HBox, 1, 1, ui::LEFT | ui::TOP);
|
dlg->currentGrid->addChildInCell(new ui::HBox, 1, 1, ui::LEFT | ui::TOP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -641,14 +676,15 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
|
||||||
if (widget->type() == ui::kButtonWidget)
|
if (widget->type() == ui::kButtonWidget)
|
||||||
hbox->enableFlags(ui::HOMOGENEOUS);
|
hbox->enableFlags(ui::HOMOGENEOUS);
|
||||||
|
|
||||||
// For tabs we don't want the empty space of an unspecified label, so
|
// For tabs and unlabeled separators, we don't want the empty space of an unspecified label, so
|
||||||
// span 2 columns.
|
// span 2 columns.
|
||||||
const int hspan = (widget->type() == Tabs::Type() ? 2 : 1);
|
const int hspan =
|
||||||
|
((widget->type() == Tabs::Type()) || (widget->type() == ui::kSeparatorWidget && !label) ? 2 :
|
||||||
|
1);
|
||||||
dlg->currentGrid->addChildInCell(hbox,
|
dlg->currentGrid->addChildInCell(hbox,
|
||||||
hspan,
|
hspan,
|
||||||
1,
|
1,
|
||||||
ui::HORIZONTAL | (vexpand ? ui::VERTICAL : ui::TOP));
|
ui::HORIZONTAL | (vexpand ? ui::VERTICAL : ui::TOP));
|
||||||
|
|
||||||
dlg->hbox = hbox;
|
dlg->hbox = hbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -709,12 +745,7 @@ int Dialog_separator(lua_State* L)
|
||||||
dlg->dataWidgets[id] = widget;
|
dlg->dataWidgets[id] = widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
dlg->mainWidgets.push_back(widget);
|
return Dialog_add_widget(L, widget);
|
||||||
dlg->currentGrid->addChildInCell(widget, 2, 1, ui::HORIZONTAL | ui::TOP);
|
|
||||||
dlg->hbox = nullptr;
|
|
||||||
|
|
||||||
lua_pushvalue(L, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int Dialog_label(lua_State* L)
|
int Dialog_label(lua_State* L)
|
||||||
|
|
@ -1048,7 +1079,7 @@ int Dialog_shades(lua_State* L)
|
||||||
int Dialog_file(lua_State* L)
|
int Dialog_file(lua_State* L)
|
||||||
{
|
{
|
||||||
std::string title = "Open File";
|
std::string title = "Open File";
|
||||||
std::string path = std::string();
|
std::string path;
|
||||||
std::string fn;
|
std::string fn;
|
||||||
base::paths exts;
|
base::paths exts;
|
||||||
auto dlgType = FileSelectorType::Open;
|
auto dlgType = FileSelectorType::Open;
|
||||||
|
|
@ -1114,11 +1145,14 @@ int Dialog_file(lua_State* L)
|
||||||
|
|
||||||
// Set default path if 'basepath' is blank
|
// Set default path if 'basepath' is blank
|
||||||
if (path.empty()) {
|
if (path.empty()) {
|
||||||
const auto* doc = App::instance()->context()->activeDocument();
|
// We use the 'filename' path the relative path if it was given.
|
||||||
if (doc)
|
path = base::get_file_path(fn);
|
||||||
|
if (path.empty()) {
|
||||||
|
if (const auto* doc = App::instance()->context()->activeDocument())
|
||||||
path = base::get_file_path(doc->filename());
|
path = base::get_file_path(doc->filename());
|
||||||
else
|
else
|
||||||
path = (base::get_file_path(fn).empty() ? base::get_current_path() : base::get_file_path(fn));
|
path = base::get_current_path();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the widget with the provided filename
|
// Update the widget with the provided filename
|
||||||
|
|
@ -1509,6 +1543,10 @@ int Dialog_modify(lua_State* L)
|
||||||
type = lua_getfield(L, 2, "text");
|
type = lua_getfield(L, 2, "text");
|
||||||
if (const char* s = lua_tostring(L, -1)) {
|
if (const char* s = lua_tostring(L, -1)) {
|
||||||
widget->setText(s);
|
widget->setText(s);
|
||||||
|
|
||||||
|
// Re-process mnemonics for buttons
|
||||||
|
if (widget->type() == WidgetType::kButtonWidget)
|
||||||
|
widget->processMnemonicFromText();
|
||||||
relayout = true;
|
relayout = true;
|
||||||
}
|
}
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
@ -1643,8 +1681,26 @@ int Dialog_modify(lua_State* L)
|
||||||
if (relayout && !dlg->window.isResizing()) {
|
if (relayout && !dlg->window.isResizing()) {
|
||||||
dlg->window.layout();
|
dlg->window.layout();
|
||||||
|
|
||||||
gfx::Rect bounds(dlg->window.bounds().w, dlg->window.sizeHint().h);
|
if (dlg->autofit > 0) {
|
||||||
dlg->window.expandWindow(bounds.size());
|
gfx::Rect oldBounds = dlg->window.bounds();
|
||||||
|
gfx::Size resize(oldBounds.size());
|
||||||
|
|
||||||
|
if (dlg->autofit & ui::TOP || dlg->autofit & ui::BOTTOM)
|
||||||
|
resize.h = dlg->window.sizeHint().h;
|
||||||
|
if (dlg->autofit & ui::LEFT || dlg->autofit & ui::RIGHT)
|
||||||
|
resize.w = dlg->window.sizeHint().w;
|
||||||
|
|
||||||
|
gfx::Size difference = resize - oldBounds.size();
|
||||||
|
const auto& bounds = dlg->getWindowBounds();
|
||||||
|
gfx::Rect newBounds(bounds.x, bounds.y, resize.w, resize.h);
|
||||||
|
|
||||||
|
if (dlg->autofit & ui::BOTTOM)
|
||||||
|
newBounds.y = bounds.y - difference.h;
|
||||||
|
if (dlg->autofit & ui::RIGHT)
|
||||||
|
newBounds.x = bounds.x - difference.w;
|
||||||
|
|
||||||
|
dlg->setWindowBounds(newBounds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lua_pushvalue(L, 1);
|
lua_pushvalue(L, 1);
|
||||||
|
|
@ -1866,6 +1922,27 @@ int Dialog_get_bounds(lua_State* L)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Dialog_get_sizeHint(lua_State* L)
|
||||||
|
{
|
||||||
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
push_new<gfx::Size>(L, dlg->window.sizeHint());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Dialog_get_autofit(lua_State* L)
|
||||||
|
{
|
||||||
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
lua_pushinteger(L, dlg->autofit);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Dialog_set_autofit(lua_State* L)
|
||||||
|
{
|
||||||
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
dlg->setAutofit(lua_tointeger(L, 2));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int Dialog_set_bounds(lua_State* L)
|
int Dialog_set_bounds(lua_State* L)
|
||||||
{
|
{
|
||||||
auto dlg = get_obj<Dialog>(L, 1);
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
|
@ -1909,6 +1986,8 @@ const luaL_Reg Dialog_methods[] = {
|
||||||
const Property Dialog_properties[] = {
|
const Property Dialog_properties[] = {
|
||||||
{ "data", Dialog_get_data, Dialog_set_data },
|
{ "data", Dialog_get_data, Dialog_set_data },
|
||||||
{ "bounds", Dialog_get_bounds, Dialog_set_bounds },
|
{ "bounds", Dialog_get_bounds, Dialog_set_bounds },
|
||||||
|
{ "autofit", Dialog_get_autofit, Dialog_set_autofit },
|
||||||
|
{ "sizeHint", Dialog_get_sizeHint, nullptr },
|
||||||
{ nullptr, nullptr, nullptr }
|
{ nullptr, nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -452,6 +452,7 @@ Engine::Engine() : L(luaL_newstate()), m_delegate(nullptr), m_printLastResult(fa
|
||||||
lua_setglobal(L, "FlipType");
|
lua_setglobal(L, "FlipType");
|
||||||
setfield_integer(L, "HORIZONTAL", doc::algorithm::FlipType::FlipHorizontal);
|
setfield_integer(L, "HORIZONTAL", doc::algorithm::FlipType::FlipHorizontal);
|
||||||
setfield_integer(L, "VERTICAL", doc::algorithm::FlipType::FlipVertical);
|
setfield_integer(L, "VERTICAL", doc::algorithm::FlipType::FlipVertical);
|
||||||
|
setfield_integer(L, "DIAGONAL", doc::algorithm::FlipType::FlipDiagonal);
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
lua_newtable(L);
|
lua_newtable(L);
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,10 @@ public:
|
||||||
#if ENABLE_REMAP_TILESET_EVENT
|
#if ENABLE_REMAP_TILESET_EVENT
|
||||||
RemapTileset,
|
RemapTileset,
|
||||||
#endif
|
#endif
|
||||||
|
LayerBlendMode,
|
||||||
|
LayerName,
|
||||||
|
LayerOpacity,
|
||||||
|
LayerVisibility,
|
||||||
};
|
};
|
||||||
|
|
||||||
SpriteEvents(const Sprite* sprite) : m_spriteId(sprite->id()) { doc()->add_observer(this); }
|
SpriteEvents(const Sprite* sprite) : m_spriteId(sprite->id()) { doc()->add_observer(this); }
|
||||||
|
|
@ -401,6 +405,14 @@ public:
|
||||||
else if (std::strcmp(eventName, "remaptileset") == 0)
|
else if (std::strcmp(eventName, "remaptileset") == 0)
|
||||||
return RemapTileset;
|
return RemapTileset;
|
||||||
#endif
|
#endif
|
||||||
|
else if (std::strcmp(eventName, "layerblendmode") == 0)
|
||||||
|
return LayerBlendMode;
|
||||||
|
else if (std::strcmp(eventName, "layername") == 0)
|
||||||
|
return LayerName;
|
||||||
|
else if (std::strcmp(eventName, "layeropacity") == 0)
|
||||||
|
return LayerOpacity;
|
||||||
|
else if (std::strcmp(eventName, "layervisibility") == 0)
|
||||||
|
return LayerVisibility;
|
||||||
else
|
else
|
||||||
return Unknown;
|
return Unknown;
|
||||||
}
|
}
|
||||||
|
|
@ -460,6 +472,38 @@ public:
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onLayerBlendModeChange(DocEvent& ev) override
|
||||||
|
{
|
||||||
|
call(LayerBlendMode,
|
||||||
|
{
|
||||||
|
{ "layer", ev.layer() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onLayerNameChange(DocEvent& ev) override
|
||||||
|
{
|
||||||
|
call(LayerName,
|
||||||
|
{
|
||||||
|
{ "layer", ev.layer() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onLayerOpacityChange(DocEvent& ev) override
|
||||||
|
{
|
||||||
|
call(LayerOpacity,
|
||||||
|
{
|
||||||
|
{ "layer", ev.layer() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAfterLayerVisibilityChange(DocEvent& ev) override
|
||||||
|
{
|
||||||
|
call(LayerVisibility,
|
||||||
|
{
|
||||||
|
{ "layer", ev.layer() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onAddFirstListener(EventType eventType) override
|
void onAddFirstListener(EventType eventType) override
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
#include "app/script/luacpp.h"
|
#include "app/script/luacpp.h"
|
||||||
#include "app/script/userdata.h"
|
#include "app/script/userdata.h"
|
||||||
#include "app/tx.h"
|
#include "app/tx.h"
|
||||||
|
#include "app/ui/timeline/timeline.h"
|
||||||
#include "doc/layer.h"
|
#include "doc/layer.h"
|
||||||
#include "doc/layer_tilemap.h"
|
#include "doc/layer_tilemap.h"
|
||||||
#include "doc/sprite.h"
|
#include "doc/sprite.h"
|
||||||
|
|
@ -355,6 +356,9 @@ int Layer_set_isCollapsed(lua_State* L)
|
||||||
{
|
{
|
||||||
auto layer = get_docobj<Layer>(L, 1);
|
auto layer = get_docobj<Layer>(L, 1);
|
||||||
layer->setCollapsed(lua_toboolean(L, 2));
|
layer->setCollapsed(lua_toboolean(L, 2));
|
||||||
|
|
||||||
|
if (auto* timeline = App::instance()->timeline())
|
||||||
|
timeline->refresh();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,6 +366,9 @@ int Layer_set_isExpanded(lua_State* L)
|
||||||
{
|
{
|
||||||
auto layer = get_docobj<Layer>(L, 1);
|
auto layer = get_docobj<Layer>(L, 1);
|
||||||
layer->setCollapsed(!lua_toboolean(L, 2));
|
layer->setCollapsed(!lua_toboolean(L, 2));
|
||||||
|
|
||||||
|
if (auto* timeline = App::instance()->timeline())
|
||||||
|
timeline->refresh();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,16 @@ struct Plugin {
|
||||||
|
|
||||||
class PluginCommand : public Command {
|
class PluginCommand : public Command {
|
||||||
public:
|
public:
|
||||||
PluginCommand(const std::string& id, const std::string& title, int onclickRef, int onenabledRef)
|
PluginCommand(const std::string& id,
|
||||||
|
const std::string& title,
|
||||||
|
int onclickRef,
|
||||||
|
int onenabledRef,
|
||||||
|
int oncheckedRef)
|
||||||
: Command(id.c_str(), CmdUIOnlyFlag)
|
: Command(id.c_str(), CmdUIOnlyFlag)
|
||||||
, m_title(title)
|
, m_title(title)
|
||||||
, m_onclickRef(onclickRef)
|
, m_onclickRef(onclickRef)
|
||||||
, m_onenabledRef(onenabledRef)
|
, m_onenabledRef(onenabledRef)
|
||||||
|
, m_oncheckedRef(oncheckedRef)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,28 +77,42 @@ protected:
|
||||||
bool onEnabled(Context* context) override
|
bool onEnabled(Context* context) override
|
||||||
{
|
{
|
||||||
if (m_onenabledRef) {
|
if (m_onenabledRef) {
|
||||||
|
return callScriptRef(m_onenabledRef);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onChecked(Context* context) override
|
||||||
|
{
|
||||||
|
if (m_oncheckedRef) {
|
||||||
|
return callScriptRef(m_oncheckedRef);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool callScriptRef(int ref)
|
||||||
|
{
|
||||||
|
ASSERT(ref);
|
||||||
script::Engine* engine = App::instance()->scriptEngine();
|
script::Engine* engine = App::instance()->scriptEngine();
|
||||||
lua_State* L = engine->luaState();
|
lua_State* L = engine->luaState();
|
||||||
|
|
||||||
lua_rawgeti(L, LUA_REGISTRYINDEX, m_onenabledRef);
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||||
if (lua_pcall(L, 0, 1, 0)) {
|
if (lua_pcall(L, 0, 1, 0)) {
|
||||||
if (const char* s = lua_tostring(L, -1)) {
|
if (const char* s = lua_tostring(L, -1))
|
||||||
Console().printf("Error: %s", s);
|
Console().printf("Error: %s", s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
|
||||||
bool ret = lua_toboolean(L, -1);
|
bool ret = lua_toboolean(L, -1);
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string m_title;
|
std::string m_title;
|
||||||
int m_onclickRef;
|
int m_onclickRef;
|
||||||
int m_onenabledRef;
|
int m_onenabledRef;
|
||||||
|
int m_oncheckedRef;
|
||||||
};
|
};
|
||||||
|
|
||||||
void deleteCommandIfExistent(Extension* ext, const std::string& id)
|
void deleteCommandIfExistent(Extension* ext, const std::string& id)
|
||||||
|
|
@ -126,6 +145,7 @@ int Plugin_newCommand(lua_State* L)
|
||||||
if (lua_istable(L, 2)) {
|
if (lua_istable(L, 2)) {
|
||||||
std::string id, title, group;
|
std::string id, title, group;
|
||||||
int onenabledRef = 0;
|
int onenabledRef = 0;
|
||||||
|
int oncheckedRef = 0;
|
||||||
|
|
||||||
lua_getfield(L, 2, "id");
|
lua_getfield(L, 2, "id");
|
||||||
if (const char* s = lua_tostring(L, -1)) {
|
if (const char* s = lua_tostring(L, -1)) {
|
||||||
|
|
@ -156,6 +176,14 @@ int Plugin_newCommand(lua_State* L)
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type = lua_getfield(L, 2, "onchecked");
|
||||||
|
if (type == LUA_TFUNCTION) {
|
||||||
|
oncheckedRef = luaL_ref(L, LUA_REGISTRYINDEX); // does a pop
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
type = lua_getfield(L, 2, "onclick");
|
type = lua_getfield(L, 2, "onclick");
|
||||||
if (type == LUA_TFUNCTION) {
|
if (type == LUA_TFUNCTION) {
|
||||||
int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
|
@ -164,7 +192,7 @@ int Plugin_newCommand(lua_State* L)
|
||||||
// overwriting a previous registered command)
|
// overwriting a previous registered command)
|
||||||
deleteCommandIfExistent(plugin->ext, id);
|
deleteCommandIfExistent(plugin->ext, id);
|
||||||
|
|
||||||
auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef);
|
auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef, oncheckedRef);
|
||||||
Commands::instance()->add(cmd);
|
Commands::instance()->add(cmd);
|
||||||
plugin->ext->addCommand(id);
|
plugin->ext->addCommand(id);
|
||||||
|
|
||||||
|
|
@ -172,6 +200,7 @@ int Plugin_newCommand(lua_State* L)
|
||||||
if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
|
if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
|
||||||
if (auto appMenus = AppMenus::instance()) {
|
if (auto appMenus = AppMenus::instance()) {
|
||||||
auto menuItem = std::make_unique<AppMenuItem>(title, id);
|
auto menuItem = std::make_unique<AppMenuItem>(title, id);
|
||||||
|
menuItem->processMnemonicFromText();
|
||||||
appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
|
appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ int secure_io_lines(lua_State* L);
|
||||||
int secure_io_input(lua_State* L);
|
int secure_io_input(lua_State* L);
|
||||||
int secure_io_output(lua_State* L);
|
int secure_io_output(lua_State* L);
|
||||||
int secure_os_execute(lua_State* L);
|
int secure_os_execute(lua_State* L);
|
||||||
|
int secure_os_remove(lua_State* L);
|
||||||
|
int secure_os_rename(lua_State* L);
|
||||||
int secure_package_loadlib(lua_State* L);
|
int secure_package_loadlib(lua_State* L);
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
|
|
@ -49,6 +51,8 @@ enum {
|
||||||
io_input,
|
io_input,
|
||||||
io_output,
|
io_output,
|
||||||
os_execute,
|
os_execute,
|
||||||
|
os_remove,
|
||||||
|
os_rename,
|
||||||
package_loadlib,
|
package_loadlib,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -64,6 +68,8 @@ static struct {
|
||||||
{ "io", "input", secure_io_input },
|
{ "io", "input", secure_io_input },
|
||||||
{ "io", "output", secure_io_output },
|
{ "io", "output", secure_io_output },
|
||||||
{ "os", "execute", secure_os_execute },
|
{ "os", "execute", secure_os_execute },
|
||||||
|
{ "os", "remove", secure_os_remove },
|
||||||
|
{ "os", "rename", secure_os_rename },
|
||||||
{ "package", "loadlib", secure_package_loadlib },
|
{ "package", "loadlib", secure_package_loadlib },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -185,6 +191,81 @@ int secure_os_execute(lua_State* L)
|
||||||
return replaced_functions[os_execute].origfunc(L);
|
return replaced_functions[os_execute].origfunc(L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int file_result(lua_State* L, bool result, int errorNo = 0, const std::string& fileName = "")
|
||||||
|
{
|
||||||
|
if (result) {
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
luaL_pushfail(L);
|
||||||
|
if (fileName.empty())
|
||||||
|
lua_pushstring(L, strerror(errorNo));
|
||||||
|
else
|
||||||
|
lua_pushfstring(L, "%s: %s", fileName.c_str(), strerror(errorNo));
|
||||||
|
lua_pushinteger(L, errorNo);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int secure_os_remove(lua_State* L)
|
||||||
|
{
|
||||||
|
const std::string absFilename = base::get_canonical_path(luaL_checkstring(L, 1));
|
||||||
|
if (absFilename.empty())
|
||||||
|
return file_result(L, false, ENOENT, absFilename);
|
||||||
|
|
||||||
|
if (!ask_access(L, absFilename.data(), FileAccessMode::Write, ResourceType::File))
|
||||||
|
return file_result(L, false, EACCES, absFilename);
|
||||||
|
|
||||||
|
if (base::is_directory(absFilename)) {
|
||||||
|
try {
|
||||||
|
base::remove_directory(absFilename);
|
||||||
|
return file_result(L, true);
|
||||||
|
}
|
||||||
|
catch (const std::exception&) {
|
||||||
|
return file_result(L, false, EIO, absFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
base::delete_file(absFilename);
|
||||||
|
}
|
||||||
|
catch (const std::exception&) {
|
||||||
|
return file_result(L, false, EIO, absFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_result(L, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int secure_os_rename(lua_State* L)
|
||||||
|
{
|
||||||
|
const std::string absSourceFilename = base::get_canonical_path(luaL_checkstring(L, 1));
|
||||||
|
const std::string absDestFilename = base::get_absolute_path(luaL_checkstring(L, 2));
|
||||||
|
lua_pop(L, 2);
|
||||||
|
|
||||||
|
if (absSourceFilename.empty())
|
||||||
|
return file_result(L, false, ENOENT, absSourceFilename);
|
||||||
|
|
||||||
|
if (absDestFilename.empty())
|
||||||
|
return file_result(L, false, EINVAL, absDestFilename);
|
||||||
|
|
||||||
|
if (!ask_access(L, absSourceFilename.data(), FileAccessMode::Write, ResourceType::File))
|
||||||
|
return file_result(L, false, EACCES, absSourceFilename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If the destination file already exists, we should ask for permission to overwrite it.
|
||||||
|
if (!base::get_canonical_path(absDestFilename).empty() &&
|
||||||
|
!ask_access(L, absDestFilename.data(), FileAccessMode::Write, ResourceType::File)) {
|
||||||
|
return file_result(L, false, EACCES, absDestFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
base::move_file(absSourceFilename, absDestFilename);
|
||||||
|
return file_result(L, true);
|
||||||
|
}
|
||||||
|
catch (const std::exception&) {
|
||||||
|
return file_result(L, false, EIO, absSourceFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int secure_package_loadlib(lua_State* L)
|
int secure_package_loadlib(lua_State* L)
|
||||||
{
|
{
|
||||||
const char* cmd = luaL_checkstring(L, 1);
|
const char* cmd = luaL_checkstring(L, 1);
|
||||||
|
|
@ -201,7 +282,7 @@ void overwrite_unsecure_functions(lua_State* L)
|
||||||
{
|
{
|
||||||
// Remove unsupported functions
|
// Remove unsupported functions
|
||||||
lua_getglobal(L, "os");
|
lua_getglobal(L, "os");
|
||||||
for (const char* name : { "remove", "rename", "exit", "tmpname" }) {
|
for (const char* name : { "exit", "tmpname" }) {
|
||||||
lua_pushcfunction(L, unsupported);
|
lua_pushcfunction(L, unsupported);
|
||||||
lua_setfield(L, -2, name);
|
lua_setfield(L, -2, name);
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +361,15 @@ bool ask_access(lua_State* L,
|
||||||
{
|
{
|
||||||
std::string label;
|
std::string label;
|
||||||
switch (resourceType) {
|
switch (resourceType) {
|
||||||
case ResourceType::File: label = Strings::script_access_file_label(); break;
|
case ResourceType::File: {
|
||||||
|
if (mode == FileAccessMode::Write) {
|
||||||
|
label = Strings::script_access_file_write_label();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
label = Strings::script_access_file_label();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ResourceType::Command: label = Strings::script_access_command_label(); break;
|
case ResourceType::Command: label = Strings::script_access_command_label(); break;
|
||||||
case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break;
|
case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break;
|
||||||
case ResourceType::Clipboard: label = Strings::script_access_clipboard_label(); break;
|
case ResourceType::Clipboard: label = Strings::script_access_clipboard_label(); break;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
#include "app/context.h"
|
#include "app/context.h"
|
||||||
#include "app/context_observer.h"
|
#include "app/context_observer.h"
|
||||||
|
#include "app/doc.h"
|
||||||
|
#include "app/doc_undo.h"
|
||||||
#include "app/script/docobj.h"
|
#include "app/script/docobj.h"
|
||||||
#include "app/script/engine.h"
|
#include "app/script/engine.h"
|
||||||
#include "app/script/luacpp.h"
|
#include "app/script/luacpp.h"
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
#include "doc/tag.h"
|
#include "doc/tag.h"
|
||||||
#include "doc/tileset.h"
|
#include "doc/tileset.h"
|
||||||
#include "doc/tilesets.h"
|
#include "doc/tilesets.h"
|
||||||
|
#include "undo/undo_state.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
@ -456,7 +457,7 @@ int Sprite_newCel(lua_State* L)
|
||||||
auto sprite = get_docobj<Sprite>(L, 1);
|
auto sprite = get_docobj<Sprite>(L, 1);
|
||||||
auto layerBase = get_docobj<Layer>(L, 2);
|
auto layerBase = get_docobj<Layer>(L, 2);
|
||||||
if (!layerBase->isImage())
|
if (!layerBase->isImage())
|
||||||
return luaL_error(L, "unexpected kinf of layer in Sprite:newCel()");
|
return luaL_error(L, "unexpected kind of layer in Sprite:newCel()");
|
||||||
|
|
||||||
frame_t frame = get_frame_number_from_arg(L, 3);
|
frame_t frame = get_frame_number_from_arg(L, 3);
|
||||||
if (frame < 0 || frame > sprite->lastFrame())
|
if (frame < 0 || frame > sprite->lastFrame())
|
||||||
|
|
@ -1029,6 +1030,42 @@ int Sprite_set_useLayerUuids(lua_State* L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Sprite_get_undoHistory(lua_State* L)
|
||||||
|
{
|
||||||
|
const auto* sprite = get_docobj<Sprite>(L, 1);
|
||||||
|
const auto* doc = static_cast<Doc*>(sprite->document());
|
||||||
|
const auto* history = doc->undoHistory();
|
||||||
|
|
||||||
|
if (!history) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const undo::UndoState* currentState = history->currentState();
|
||||||
|
const undo::UndoState* s = history->firstState();
|
||||||
|
const bool canRedo = history->canRedo();
|
||||||
|
bool pastCurrent = !currentState && canRedo;
|
||||||
|
|
||||||
|
int undoSteps = 0;
|
||||||
|
int redoSteps = 0;
|
||||||
|
while (s) {
|
||||||
|
if (pastCurrent && canRedo)
|
||||||
|
redoSteps++;
|
||||||
|
else if (currentState || !canRedo)
|
||||||
|
undoSteps++;
|
||||||
|
|
||||||
|
if (s == currentState || !currentState)
|
||||||
|
pastCurrent = true;
|
||||||
|
|
||||||
|
s = s->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_newtable(L);
|
||||||
|
setfield_integer(L, "undoSteps", undoSteps);
|
||||||
|
setfield_integer(L, "redoSteps", redoSteps);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
const luaL_Reg Sprite_methods[] = {
|
const luaL_Reg Sprite_methods[] = {
|
||||||
{ "__eq", Sprite_eq },
|
{ "__eq", Sprite_eq },
|
||||||
{ "resize", Sprite_resize },
|
{ "resize", Sprite_resize },
|
||||||
|
|
@ -1094,6 +1131,7 @@ const Property Sprite_properties[] = {
|
||||||
{ "events", Sprite_get_events, nullptr },
|
{ "events", Sprite_get_events, nullptr },
|
||||||
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
|
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
|
||||||
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
|
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
|
||||||
|
{ "undoHistory", Sprite_get_undoHistory, nullptr },
|
||||||
{ nullptr, nullptr, nullptr }
|
{ nullptr, nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@
|
||||||
#include "app/ui/keyboard_shortcuts.h"
|
#include "app/ui/keyboard_shortcuts.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "os/menus.h"
|
#include "os/menus.h"
|
||||||
#include "ui/accelerator.h"
|
|
||||||
#include "ui/menu.h"
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/scale.h"
|
#include "ui/scale.h"
|
||||||
|
#include "ui/shortcut.h"
|
||||||
#include "ui/size_hint_event.h"
|
#include "ui/size_hint_event.h"
|
||||||
#include "ui/widget.h"
|
#include "ui/widget.h"
|
||||||
|
|
||||||
|
|
@ -139,8 +139,8 @@ void AppMenuItem::onSizeHint(SizeHintEvent& ev)
|
||||||
border().width();
|
border().width();
|
||||||
size.h = textHeight() + border().height();
|
size.h = textHeight() + border().height();
|
||||||
|
|
||||||
if (m_key && !m_key->accels().empty()) {
|
if (m_key && !m_key->shortcuts().empty()) {
|
||||||
size.w += font()->textLength(m_key->accels().front().toString());
|
size.w += font()->textLength(m_key->shortcuts().front().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -431,8 +431,8 @@ void BrushPopup::regenerate(ui::Display* display, const gfx::Point& pos)
|
||||||
params.set("change", "custom");
|
params.set("change", "custom");
|
||||||
params.set("slot", base::convert_to<std::string>(slot).c_str());
|
params.set("slot", base::convert_to<std::string>(slot).c_str());
|
||||||
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
|
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
|
||||||
if (key && !key->accels().empty())
|
if (key && !key->shortcuts().empty())
|
||||||
shortcut = key->accels().front().toString();
|
shortcut = key->shortcuts().front().toString();
|
||||||
}
|
}
|
||||||
m_customBrushes->addItem(new SelectBrushItem(brush, slot));
|
m_customBrushes->addItem(new SelectBrushItem(brush, slot));
|
||||||
m_customBrushes->addItem(new BrushShortcutItem(shortcut, slot));
|
m_customBrushes->addItem(new BrushShortcutItem(shortcut, slot));
|
||||||
|
|
|
||||||
|
|
@ -1001,12 +1001,12 @@ public:
|
||||||
m_angle.setSuffix("°");
|
m_angle.setSuffix("°");
|
||||||
m_skew.setSuffix("°");
|
m_skew.setSuffix("°");
|
||||||
|
|
||||||
addChild(new Label("P:"));
|
addChild(new Label(Strings::context_bar_position_label()));
|
||||||
addChild(&m_x);
|
addChild(&m_x);
|
||||||
addChild(&m_y);
|
addChild(&m_y);
|
||||||
addChild(&m_w);
|
addChild(&m_w);
|
||||||
addChild(&m_h);
|
addChild(&m_h);
|
||||||
addChild(new Label("R:"));
|
addChild(new Label(Strings::context_bar_rotation_label()));
|
||||||
addChild(&m_angle);
|
addChild(&m_angle);
|
||||||
addChild(&m_skew);
|
addChild(&m_skew);
|
||||||
|
|
||||||
|
|
@ -1047,6 +1047,16 @@ public:
|
||||||
m_skew.Change.connect([this] { onChangeSkew(); });
|
m_skew.Change.connect([this] { onChangeSkew(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setupTooltips(TooltipManager* tooltipManager)
|
||||||
|
{
|
||||||
|
tooltipManager->addTooltipFor(&m_x, Strings::context_bar_position_x(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_y, Strings::context_bar_position_y(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_w, Strings::context_bar_size_width(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_h, Strings::context_bar_size_height(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_angle, Strings::context_bar_rotation_angle(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_skew, Strings::context_bar_rotation_skew(), BOTTOM);
|
||||||
|
}
|
||||||
|
|
||||||
void update(const Transformation& t)
|
void update(const Transformation& t)
|
||||||
{
|
{
|
||||||
auto rc = t.bounds();
|
auto rc = t.bounds();
|
||||||
|
|
@ -1380,11 +1390,12 @@ public:
|
||||||
|
|
||||||
class ContextBar::DropPixelsField : public ButtonSet {
|
class ContextBar::DropPixelsField : public ButtonSet {
|
||||||
public:
|
public:
|
||||||
DropPixelsField() : ButtonSet(2)
|
DropPixelsField() : ButtonSet(3)
|
||||||
{
|
{
|
||||||
auto* theme = SkinTheme::get(this);
|
auto* theme = SkinTheme::get(this);
|
||||||
|
|
||||||
addItem(theme->parts.dropPixelsOk(), theme->styles.contextBarButton());
|
addItem(theme->parts.dropPixelsOk(), theme->styles.contextBarButton());
|
||||||
|
addItem(theme->parts.dropPixelsDrop(), theme->styles.contextBarButton());
|
||||||
addItem(theme->parts.dropPixelsCancel(), theme->styles.contextBarButton());
|
addItem(theme->parts.dropPixelsCancel(), theme->styles.contextBarButton());
|
||||||
setOfferCapture(false);
|
setOfferCapture(false);
|
||||||
}
|
}
|
||||||
|
|
@ -1392,8 +1403,26 @@ public:
|
||||||
void setupTooltips(TooltipManager* tooltipManager)
|
void setupTooltips(TooltipManager* tooltipManager)
|
||||||
{
|
{
|
||||||
// TODO Enter and Esc should be configurable keys
|
// TODO Enter and Esc should be configurable keys
|
||||||
tooltipManager->addTooltipFor(at(0), Strings::context_bar_drop_pixel(), BOTTOM);
|
|
||||||
tooltipManager->addTooltipFor(at(1), Strings::context_bar_cancel_drag(), BOTTOM);
|
tooltipManager->addTooltipFor(
|
||||||
|
at(0),
|
||||||
|
key_tooltip(Strings::context_bar_drop_pixel_and_deselect().c_str(),
|
||||||
|
CommandId::DeselectMask(),
|
||||||
|
{},
|
||||||
|
KeyContext::Transformation),
|
||||||
|
BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(at(1),
|
||||||
|
key_tooltip(Strings::context_bar_drop_pixel().c_str(),
|
||||||
|
CommandId::Apply(),
|
||||||
|
{},
|
||||||
|
KeyContext::Transformation),
|
||||||
|
BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(at(2),
|
||||||
|
key_tooltip(Strings::context_bar_cancel_drag().c_str(),
|
||||||
|
CommandId::Undo(),
|
||||||
|
{},
|
||||||
|
KeyContext::Transformation),
|
||||||
|
BOTTOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
|
obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
|
||||||
|
|
@ -1404,8 +1433,9 @@ protected:
|
||||||
ButtonSet::onItemChange(item);
|
ButtonSet::onItemChange(item);
|
||||||
|
|
||||||
switch (selectedItem()) {
|
switch (selectedItem()) {
|
||||||
case 0: DropPixels(ContextBarObserver::DropPixels); break;
|
case 0: DropPixels(ContextBarObserver::Deselect); break;
|
||||||
case 1: DropPixels(ContextBarObserver::CancelDrag); break;
|
case 1: DropPixels(ContextBarObserver::DropPixels); break;
|
||||||
|
case 2: DropPixels(ContextBarObserver::CancelDrag); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1857,7 +1887,7 @@ private:
|
||||||
|
|
||||||
class ContextBar::FontSelector : public FontEntry {
|
class ContextBar::FontSelector : public FontEntry {
|
||||||
public:
|
public:
|
||||||
FontSelector(ContextBar* contextBar)
|
FontSelector(ContextBar* contextBar) : FontEntry(true) // With stroke and fill options
|
||||||
{
|
{
|
||||||
// Load the font from the preferences
|
// Load the font from the preferences
|
||||||
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
|
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
|
||||||
|
|
@ -2559,6 +2589,11 @@ FontInfo ContextBar::fontInfo() const
|
||||||
return m_fontSelector->info();
|
return m_fontSelector->info();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FontEntry* ContextBar::fontEntry()
|
||||||
|
{
|
||||||
|
return m_fontSelector;
|
||||||
|
}
|
||||||
|
|
||||||
render::DitheringMatrix ContextBar::ditheringMatrix()
|
render::DitheringMatrix ContextBar::ditheringMatrix()
|
||||||
{
|
{
|
||||||
return m_ditheringSelector->ditheringMatrix();
|
return m_ditheringSelector->ditheringMatrix();
|
||||||
|
|
@ -2621,6 +2656,7 @@ void ContextBar::setupTooltips(TooltipManager* tooltipManager)
|
||||||
m_dropPixels->setupTooltips(tooltipManager);
|
m_dropPixels->setupTooltips(tooltipManager);
|
||||||
m_symmetry->setupTooltips(tooltipManager);
|
m_symmetry->setupTooltips(tooltipManager);
|
||||||
m_sliceFields->setupTooltips(tooltipManager);
|
m_sliceFields->setupTooltips(tooltipManager);
|
||||||
|
m_transformation->setupTooltips(tooltipManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ContextBar::registerCommands()
|
void ContextBar::registerCommands()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -90,6 +90,7 @@ public:
|
||||||
|
|
||||||
// For text tool
|
// For text tool
|
||||||
FontInfo fontInfo() const;
|
FontInfo fontInfo() const;
|
||||||
|
FontEntry* fontEntry();
|
||||||
|
|
||||||
// For gradients
|
// For gradients
|
||||||
render::DitheringMatrix ditheringMatrix();
|
render::DitheringMatrix ditheringMatrix();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2015 David Capello
|
// Copyright (C) 2001-2015 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -12,7 +13,7 @@ namespace app {
|
||||||
|
|
||||||
class ContextBarObserver {
|
class ContextBarObserver {
|
||||||
public:
|
public:
|
||||||
enum DropAction { DropPixels, CancelDrag };
|
enum DropAction { Deselect, DropPixels, CancelDrag };
|
||||||
|
|
||||||
virtual ~ContextBarObserver() {}
|
virtual ~ContextBarObserver() {}
|
||||||
virtual void onDropPixels(DropAction action) {}
|
virtual void onDropPixels(DropAction action) {}
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ DitheringSelector::DitheringSelector(Type type) : m_type(type)
|
||||||
m_extChanges = extensions.DitheringMatricesChange.connect([this] { regenerate(); });
|
m_extChanges = extensions.DitheringMatricesChange.connect([this] { regenerate(); });
|
||||||
|
|
||||||
setUseCustomWidget(true);
|
setUseCustomWidget(true);
|
||||||
|
regenerate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
|
void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
|
||||||
|
|
@ -189,16 +190,6 @@ void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
|
||||||
setSizeHint(calcItemSizeHint(0));
|
setSizeHint(calcItemSizeHint(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DitheringSelector::onVisible(bool visible)
|
|
||||||
{
|
|
||||||
if (visible && !m_initialized) {
|
|
||||||
// Only do the expensive regeneration when we're set as visible for the first time.
|
|
||||||
regenerate();
|
|
||||||
m_initialized = true;
|
|
||||||
}
|
|
||||||
ComboBox::onVisible(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DitheringSelector::setSelectedItemByName(const std::string& name)
|
void DitheringSelector::setSelectedItemByName(const std::string& name)
|
||||||
{
|
{
|
||||||
int index = findItemIndex(name);
|
int index = findItemIndex(name);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||||
void onVisible(bool visible) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void regenerate(int selectedItemIndex = 0);
|
void regenerate(int selectedItemIndex = 0);
|
||||||
|
|
@ -39,7 +38,6 @@ private:
|
||||||
|
|
||||||
Type m_type;
|
Type m_type;
|
||||||
obs::scoped_connection m_extChanges;
|
obs::scoped_connection m_extChanges;
|
||||||
bool m_initialized = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -36,15 +36,18 @@
|
||||||
#include "app/ui/workspace.h"
|
#include "app/ui/workspace.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "app/util/clipboard.h"
|
#include "app/util/clipboard.h"
|
||||||
|
#include "app/util/slice_utils.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
#include "doc/color.h"
|
#include "doc/color.h"
|
||||||
#include "doc/layer.h"
|
#include "doc/layer.h"
|
||||||
|
#include "doc/slice.h"
|
||||||
#include "doc/sprite.h"
|
#include "doc/sprite.h"
|
||||||
#include "fmt/format.h"
|
#include "fmt/format.h"
|
||||||
#include "ui/accelerator.h"
|
|
||||||
#include "ui/alert.h"
|
#include "ui/alert.h"
|
||||||
|
#include "ui/display.h"
|
||||||
#include "ui/menu.h"
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
|
#include "ui/shortcut.h"
|
||||||
#include "ui/system.h"
|
#include "ui/system.h"
|
||||||
#include "ui/view.h"
|
#include "ui/view.h"
|
||||||
|
|
||||||
|
|
@ -147,11 +150,11 @@ protected:
|
||||||
KeyPtr rmb = keys->action(KeyAction::RightMouseButton, KeyContext::Any);
|
KeyPtr rmb = keys->action(KeyAction::RightMouseButton, KeyContext::Any);
|
||||||
|
|
||||||
// Convert action keys into mouse messages.
|
// Convert action keys into mouse messages.
|
||||||
if (lmb->isPressed(msg, *keys) || rmb->isPressed(msg, *keys)) {
|
if (lmb->isPressed(msg) || rmb->isPressed(msg)) {
|
||||||
MouseMessage mouseMsg(
|
MouseMessage mouseMsg(
|
||||||
(msg->type() == kKeyDownMessage ? kMouseDownMessage : kMouseUpMessage),
|
(msg->type() == kKeyDownMessage ? kMouseDownMessage : kMouseUpMessage),
|
||||||
PointerType::Unknown,
|
PointerType::Unknown,
|
||||||
(lmb->isPressed(msg, *keys) ? kButtonLeft : kButtonRight),
|
(lmb->isPressed(msg) ? kButtonLeft : kButtonRight),
|
||||||
msg->modifiers(),
|
msg->modifiers(),
|
||||||
mousePosInDisplay());
|
mousePosInDisplay());
|
||||||
|
|
||||||
|
|
@ -306,6 +309,11 @@ bool DocView::onCloseView(Workspace* workspace, bool quitting)
|
||||||
|
|
||||||
// See if the sprite has changes
|
// See if the sprite has changes
|
||||||
while (m_document->isModified()) {
|
while (m_document->isModified()) {
|
||||||
|
if (quitting) {
|
||||||
|
// Make sure the window is active so we can see the message when we close the app.
|
||||||
|
display()->nativeWindow()->activate();
|
||||||
|
}
|
||||||
|
|
||||||
// ask what want to do the user with the changes in the sprite
|
// ask what want to do the user with the changes in the sprite
|
||||||
int ret = Alert::show(Strings::alerts_save_sprite_changes(
|
int ret = Alert::show(Strings::alerts_save_sprite_changes(
|
||||||
m_document->name(),
|
m_document->name(),
|
||||||
|
|
@ -510,6 +518,8 @@ bool DocView::onCanCopy(Context* ctx)
|
||||||
return true;
|
return true;
|
||||||
else if (m_editor->isMovingPixels())
|
else if (m_editor->isMovingPixels())
|
||||||
return true;
|
return true;
|
||||||
|
else if (m_editor->hasSelectedSlices())
|
||||||
|
return true;
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -528,6 +538,11 @@ bool DocView::onCanPaste(Context* ctx)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable) &&
|
||||||
|
ctx->clipboard()->format() == ClipboardFormat::Slices) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,7 +575,13 @@ bool DocView::onCopy(Context* ctx)
|
||||||
ctx->clipboard()->copy(reader);
|
ctx->clipboard()->copy(reader);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
std::vector<Slice*> selectedSlices = get_selected_slices(reader.site());
|
||||||
|
if (!selectedSlices.empty()) {
|
||||||
|
ctx->clipboard()->copySlices(selectedSlices);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -568,7 +589,8 @@ bool DocView::onPaste(Context* ctx, const gfx::Point* position)
|
||||||
{
|
{
|
||||||
auto clipboard = ctx->clipboard();
|
auto clipboard = ctx->clipboard();
|
||||||
if (clipboard->format() == ClipboardFormat::Image ||
|
if (clipboard->format() == ClipboardFormat::Image ||
|
||||||
clipboard->format() == ClipboardFormat::Tilemap) {
|
clipboard->format() == ClipboardFormat::Tilemap ||
|
||||||
|
clipboard->format() == ClipboardFormat::Slices) {
|
||||||
clipboard->paste(ctx, true, position);
|
clipboard->paste(ctx, true, position);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -457,6 +457,8 @@ void BrushPreview::show(const gfx::Point& screenPos)
|
||||||
|
|
||||||
// Here we re-use the cached surface
|
// Here we re-use the cached surface
|
||||||
if (!cached && m_uiLayer->surface()) {
|
if (!cached && m_uiLayer->surface()) {
|
||||||
|
m_uiLayer->surface()->clear();
|
||||||
|
|
||||||
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
|
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
|
||||||
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);
|
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
#include "app/util/tile_flags_utils.h"
|
#include "app/util/tile_flags_utils.h"
|
||||||
#include "base/chrono.h"
|
#include "base/chrono.h"
|
||||||
#include "base/convert_to.h"
|
#include "base/convert_to.h"
|
||||||
|
#include "base/scoped_value.h"
|
||||||
#include "doc/doc.h"
|
#include "doc/doc.h"
|
||||||
#include "doc/mask_boundaries.h"
|
#include "doc/mask_boundaries.h"
|
||||||
#include "doc/slice.h"
|
#include "doc/slice.h"
|
||||||
|
|
@ -266,6 +267,23 @@ void Editor::setStateInternal(const EditorStatePtr& newState)
|
||||||
{
|
{
|
||||||
m_brushPreview.hide();
|
m_brushPreview.hide();
|
||||||
|
|
||||||
|
// Some onLeaveState impls (like the ones from MovingPixelsState,
|
||||||
|
// WritingTextState, MovingSelectionState) might generate a
|
||||||
|
// Tx/Transaction::commit(), which will add a new undo state,
|
||||||
|
// triggering a sprite change scripting event
|
||||||
|
// (SpriteEvents::onAddUndoState). This event could be handled by an
|
||||||
|
// extension and that extension might want to save the current
|
||||||
|
// sprite (e.g. calling Sprite_saveCopyAs, the kind of extension
|
||||||
|
// that takes snapshots after each sprite change). That will be a
|
||||||
|
// new Context::executeCommand() for the save command, generating a
|
||||||
|
// BeforeCommandExecution signal, getting back to onLeaveState
|
||||||
|
// again. In that case, we just ignore the reentry as the first
|
||||||
|
// onLeaveState should handle everything (to avoid an stack
|
||||||
|
// overflow/infinite recursion).
|
||||||
|
if (m_leavingState)
|
||||||
|
return;
|
||||||
|
base::ScopedValue leaving(m_leavingState, true);
|
||||||
|
|
||||||
// Fire before change state event, set the state, and fire after
|
// Fire before change state event, set the state, and fire after
|
||||||
// change state event.
|
// change state event.
|
||||||
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());
|
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());
|
||||||
|
|
@ -2537,6 +2555,16 @@ void Editor::onBeforeLayerEditableChange(DocEvent& ev, bool newState)
|
||||||
m_state->onBeforeLayerEditableChange(this, ev.layer(), newState);
|
m_state->onBeforeLayerEditableChange(this, ev.layer(), newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Editor::onBeforeSlicesDuplication(DocEvent& ev)
|
||||||
|
{
|
||||||
|
clearSlicesSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::onSliceDuplicated(DocEvent& ev)
|
||||||
|
{
|
||||||
|
selectSlice(ev.slice());
|
||||||
|
}
|
||||||
|
|
||||||
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
|
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
|
||||||
{
|
{
|
||||||
bool used = false;
|
bool used = false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -343,6 +343,8 @@ protected:
|
||||||
void onRemoveSlice(DocEvent& ev) override;
|
void onRemoveSlice(DocEvent& ev) override;
|
||||||
void onBeforeLayerVisibilityChange(DocEvent& ev, bool newState) override;
|
void onBeforeLayerVisibilityChange(DocEvent& ev, bool newState) override;
|
||||||
void onBeforeLayerEditableChange(DocEvent& ev, bool newState) override;
|
void onBeforeLayerEditableChange(DocEvent& ev, bool newState) override;
|
||||||
|
void onBeforeSlicesDuplication(DocEvent& ev) override;
|
||||||
|
void onSliceDuplicated(DocEvent& ev) override;
|
||||||
|
|
||||||
// ActiveToolObserver impl
|
// ActiveToolObserver impl
|
||||||
void onActiveToolChange(tools::Tool* tool) override;
|
void onActiveToolChange(tools::Tool* tool) override;
|
||||||
|
|
@ -458,6 +460,13 @@ private:
|
||||||
|
|
||||||
DocView* m_docView;
|
DocView* m_docView;
|
||||||
|
|
||||||
|
// Special flag to avoid re-entering a new state when we are leaving
|
||||||
|
// the current one. This avoids an infinite onLeaveState() recursion
|
||||||
|
// in some special cases when an extension (third-party code)
|
||||||
|
// creates a new sprite change in the same sprite change scripting
|
||||||
|
// event.
|
||||||
|
bool m_leavingState = false;
|
||||||
|
|
||||||
// Last known mouse position received by this editor when the
|
// Last known mouse position received by this editor when the
|
||||||
// mouse button was pressed. Used for auto-scrolling. To get the
|
// mouse button was pressed. Used for auto-scrolling. To get the
|
||||||
// current mouse position on the editor you can use
|
// current mouse position on the editor you can use
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
#include "app/commands/commands.h"
|
#include "app/commands/commands.h"
|
||||||
#include "app/commands/move_thing.h"
|
#include "app/commands/move_thing.h"
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/modules/gui.h"
|
#include "app/modules/gui.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/tools/ink.h"
|
#include "app/tools/ink.h"
|
||||||
|
|
@ -48,6 +49,7 @@
|
||||||
#include "fmt/format.h"
|
#include "fmt/format.h"
|
||||||
#include "gfx/rect.h"
|
#include "gfx/rect.h"
|
||||||
#include "ui/manager.h"
|
#include "ui/manager.h"
|
||||||
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/system.h"
|
#include "ui/system.h"
|
||||||
#include "ui/view.h"
|
#include "ui/view.h"
|
||||||
|
|
@ -286,9 +288,6 @@ bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
|
||||||
UIContext* ctx = UIContext::instance();
|
UIContext* ctx = UIContext::instance();
|
||||||
ctx->setActiveView(editor->getDocView());
|
ctx->setActiveView(editor->getDocView());
|
||||||
|
|
||||||
ContextBar* contextBar = App::instance()->contextBar();
|
|
||||||
contextBar->updateForMovingPixels(getTransformation(editor));
|
|
||||||
|
|
||||||
// Start scroll loop
|
// Start scroll loop
|
||||||
if (editor->checkForScroll(msg) || editor->checkForZoom(msg))
|
if (editor->checkForScroll(msg) || editor->checkForZoom(msg))
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -442,10 +441,6 @@ void MovingPixelsState::onCommitMouseMove(Editor* editor, const gfx::PointF& spr
|
||||||
// Drag the image to that position
|
// Drag the image to that position
|
||||||
m_pixelsMovement->moveImage(spritePos, moveModifier);
|
m_pixelsMovement->moveImage(spritePos, moveModifier);
|
||||||
|
|
||||||
// Update context bar and status bar
|
|
||||||
ContextBar* contextBar = App::instance()->contextBar();
|
|
||||||
contextBar->updateForMovingPixels(transformation);
|
|
||||||
|
|
||||||
m_editor->updateStatusBar();
|
m_editor->updateStatusBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,19 +470,6 @@ bool MovingPixelsState::onKeyDown(Editor* editor, KeyMessage* msg)
|
||||||
// FineControl now (e.g. if we pressed another modifier key).
|
// FineControl now (e.g. if we pressed another modifier key).
|
||||||
m_lockedKeyAction = KeyAction::None;
|
m_lockedKeyAction = KeyAction::None;
|
||||||
|
|
||||||
if (msg->scancode() == kKeyEnter || // TODO make this key customizable
|
|
||||||
msg->scancode() == kKeyEnterPad || msg->scancode() == kKeyEsc) {
|
|
||||||
dropPixels();
|
|
||||||
|
|
||||||
// The escape key drop pixels and deselect the mask.
|
|
||||||
if (msg->scancode() == kKeyEsc) { // TODO make this key customizable
|
|
||||||
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
|
|
||||||
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use StandbyState implementation
|
// Use StandbyState implementation
|
||||||
return StandbyState::onKeyDown(editor, msg);
|
return StandbyState::onKeyDown(editor, msg);
|
||||||
}
|
}
|
||||||
|
|
@ -529,6 +511,10 @@ bool MovingPixelsState::onUpdateStatusBar(Editor* editor)
|
||||||
const Transformation& transform(getTransformation(editor));
|
const Transformation& transform(getTransformation(editor));
|
||||||
gfx::Size imageSize = m_pixelsMovement->getInitialImageSize();
|
gfx::Size imageSize = m_pixelsMovement->getInitialImageSize();
|
||||||
|
|
||||||
|
// Update the context bar along with the status bar
|
||||||
|
ContextBar* contextBar = App::instance()->contextBar();
|
||||||
|
contextBar->updateForMovingPixels(transform);
|
||||||
|
|
||||||
int w = int(transform.bounds().w);
|
int w = int(transform.bounds().w);
|
||||||
int h = int(transform.bounds().h);
|
int h = int(transform.bounds().h);
|
||||||
int gcd = base::gcd(w, h);
|
int gcd = base::gcd(w, h);
|
||||||
|
|
@ -634,6 +620,12 @@ void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Handle undo directly as cancelDrag() to avoid adding an action in the history.
|
||||||
|
else if (command->id() == CommandId::Undo()) {
|
||||||
|
cancelDrag();
|
||||||
|
ev.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Don't drop pixels if the user zooms/scrolls/picks a color
|
// Don't drop pixels if the user zooms/scrolls/picks a color
|
||||||
// using commands.
|
// using commands.
|
||||||
else if ((command->id() == CommandId::Zoom()) || (command->id() == CommandId::Scroll()) ||
|
else if ((command->id() == CommandId::Zoom()) || (command->id() == CommandId::Scroll()) ||
|
||||||
|
|
@ -795,16 +787,33 @@ void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case ContextBarObserver::Deselect: deselect(); break;
|
||||||
case ContextBarObserver::DropPixels: dropPixels(); break;
|
case ContextBarObserver::DropPixels: dropPixels(); break;
|
||||||
|
case ContextBarObserver::CancelDrag: cancelDrag(); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MovingPixelsState::deselect()
|
||||||
|
{
|
||||||
|
if (!m_pixelsMovement || m_discarded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
dropPixels();
|
||||||
|
|
||||||
|
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
|
||||||
|
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MovingPixelsState::cancelDrag()
|
||||||
|
{
|
||||||
|
if (!m_pixelsMovement || m_discarded)
|
||||||
|
return;
|
||||||
|
|
||||||
case ContextBarObserver::CancelDrag:
|
|
||||||
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
|
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
|
||||||
m_discarded = true;
|
m_discarded = true;
|
||||||
|
|
||||||
// Quit from MovingPixelsState, back to standby.
|
// Quit from MovingPixelsState, back to standby.
|
||||||
m_editor->backToPreviousState();
|
dropPixels();
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MovingPixelsState::onPivotChange()
|
void MovingPixelsState::onPivotChange()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -94,7 +94,10 @@ private:
|
||||||
void onBeforeCommandExecution(CommandExecutionEvent& ev);
|
void onBeforeCommandExecution(CommandExecutionEvent& ev);
|
||||||
|
|
||||||
void setTransparentColor(bool opaque, const app::Color& color);
|
void setTransparentColor(bool opaque, const app::Color& color);
|
||||||
|
|
||||||
|
void deselect();
|
||||||
void dropPixels();
|
void dropPixels();
|
||||||
|
void cancelDrag();
|
||||||
|
|
||||||
bool isActiveDocument() const;
|
bool isActiveDocument() const;
|
||||||
bool isActiveEditor() const;
|
bool isActiveEditor() const;
|
||||||
|
|
|
||||||
|
|
@ -428,8 +428,8 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
|
||||||
if (editor->slicesTransforms())
|
if (editor->slicesTransforms())
|
||||||
drawExtraCel();
|
drawExtraCel();
|
||||||
|
|
||||||
// Redraw the editor.
|
// Notify changes
|
||||||
editor->invalidate();
|
m_site.document()->notifyGeneralUpdate();
|
||||||
|
|
||||||
// Use StandbyState implementation
|
// Use StandbyState implementation
|
||||||
return StandbyState::onMouseMove(editor, msg);
|
return StandbyState::onMouseMove(editor, msg);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -420,10 +420,12 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
|
||||||
// Now we calculate the difference from x1,y1 point and we can
|
// Now we calculate the difference from x1,y1 point and we can
|
||||||
// use it to adjust all coordinates (x1, y1, x2, y2).
|
// use it to adjust all coordinates (x1, y1, x2, y2).
|
||||||
bounds.setOrigin(gridOffset);
|
bounds.setOrigin(gridOffset);
|
||||||
|
newTransformation.pivot(abs_initial_pivot - m_initialData.bounds().origin() + gridOffset);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
newTransformation.pivot(abs_initial_pivot + gfx::PointF(dx, dy));
|
||||||
|
|
||||||
newTransformation.bounds(bounds);
|
newTransformation.bounds(bounds);
|
||||||
newTransformation.pivot(abs_initial_pivot + gfx::PointF(dx, dy));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -464,8 +464,7 @@ bool StandbyState::onKeyDown(Editor* editor, KeyMessage* msg)
|
||||||
checkStartDrawingStraightLine(editor, nullptr, nullptr))
|
checkStartDrawingStraightLine(editor, nullptr, nullptr))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Keys keys = KeyboardShortcuts::instance()->getDragActionsFromKeyMessage(KeyContext::MouseWheel,
|
Keys keys = KeyboardShortcuts::instance()->getDragActionsFromKeyMessage(msg);
|
||||||
msg);
|
|
||||||
if (editor->hasMouse() && !keys.empty()) {
|
if (editor->hasMouse() && !keys.empty()) {
|
||||||
// Don't enter DraggingValueState to change brush size if we are
|
// Don't enter DraggingValueState to change brush size if we are
|
||||||
// in a selection-like tool
|
// in a selection-like tool
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,11 @@ bool StateWithWheelBehavior::onMouseWheel(Editor* editor, MouseMessage* msg)
|
||||||
double dz = delta.x + delta.y;
|
double dz = delta.x + delta.y;
|
||||||
WheelAction wheelAction = WheelAction::None;
|
WheelAction wheelAction = WheelAction::None;
|
||||||
|
|
||||||
if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
|
if (tools::Tool* quickTool = App::instance()->activeToolManager()->quickTool();
|
||||||
|
quickTool && quickTool->getId() == tools::WellKnownInks::Zoom) {
|
||||||
|
wheelAction = WheelAction::Zoom;
|
||||||
|
}
|
||||||
|
else if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
|
||||||
if (!Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel())
|
if (!Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel())
|
||||||
wheelAction = WheelAction::VScroll;
|
wheelAction = WheelAction::VScroll;
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "app/util/expand_cel_canvas.h"
|
#include "app/util/expand_cel_canvas.h"
|
||||||
#include "app/util/layer_utils.h"
|
#include "app/util/layer_utils.h"
|
||||||
|
#include "app/util/slice_utils.h"
|
||||||
#include "doc/brush.h"
|
#include "doc/brush.h"
|
||||||
#include "doc/cel.h"
|
#include "doc/cel.h"
|
||||||
#include "doc/image.h"
|
#include "doc/image.h"
|
||||||
|
|
@ -693,7 +694,7 @@ public:
|
||||||
// popup menu to create a new one.
|
// popup menu to create a new one.
|
||||||
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
|
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
|
||||||
Slice* slice = new Slice;
|
Slice* slice = new Slice;
|
||||||
slice->setName(getUniqueSliceName());
|
slice->setName(get_unique_slice_name(m_sprite));
|
||||||
|
|
||||||
SliceKey key(bounds);
|
SliceKey key(bounds);
|
||||||
slice->insert(getFrame(), key);
|
slice->insert(getFrame(), key);
|
||||||
|
|
@ -716,18 +717,6 @@ private:
|
||||||
// EditorObserver impl
|
// EditorObserver impl
|
||||||
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||||
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||||
|
|
||||||
std::string getUniqueSliceName() const
|
|
||||||
{
|
|
||||||
std::string prefix = "Slice";
|
|
||||||
int max = 0;
|
|
||||||
|
|
||||||
for (Slice* slice : m_sprite->slices())
|
|
||||||
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
|
|
||||||
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
|
|
||||||
|
|
||||||
return fmt::format("{} {}", prefix, max + 1);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2022 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -209,7 +209,8 @@ gfx::Rect TransformHandles::getPivotHandleBounds(Editor* editor,
|
||||||
{
|
{
|
||||||
auto theme = SkinTheme::get(editor);
|
auto theme = SkinTheme::get(editor);
|
||||||
gfx::Size partSize = theme->parts.pivotHandle()->size();
|
gfx::Size partSize = theme->parts.pivotHandle()->size();
|
||||||
gfx::Point screenPivotPos = editor->editorToScreen(gfx::Point(transform.pivot()));
|
gfx::Point pivotPos = gfx::Point(transform.pivot()) + editor->mainTilePosition();
|
||||||
|
gfx::Point screenPivotPos = editor->editorToScreen(pivotPos);
|
||||||
|
|
||||||
screenPivotPos.x += editor->projection().applyX(1) / 2;
|
screenPivotPos.x += editor->projection().applyX(1) / 2;
|
||||||
screenPivotPos.y += editor->projection().applyY(1) / 2;
|
screenPivotPos.y += editor->projection().applyY(1) / 2;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include "app/commands/command.h"
|
#include "app/commands/command.h"
|
||||||
#include "app/extra_cel.h"
|
#include "app/extra_cel.h"
|
||||||
#include "app/fonts/font_info.h"
|
#include "app/fonts/font_info.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/site.h"
|
#include "app/site.h"
|
||||||
#include "app/tx.h"
|
#include "app/tx.h"
|
||||||
|
|
@ -43,12 +44,42 @@
|
||||||
#include "os/skia/skia_surface.h"
|
#include "os/skia/skia_surface.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
using namespace ui;
|
using namespace ui;
|
||||||
|
|
||||||
|
// Get ui::Paint to render text from context bar options / preferences
|
||||||
|
static ui::Paint get_paint_for_text()
|
||||||
|
{
|
||||||
|
ui::Paint paint;
|
||||||
|
if (auto* app = App::instance()) {
|
||||||
|
if (auto* ctxBar = app->contextBar())
|
||||||
|
paint = ctxBar->fontEntry()->paint();
|
||||||
|
}
|
||||||
|
paint.color(color_utils::color_for_ui(Preferences::instance().colorBar.fgColor()));
|
||||||
|
return paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gfx::RectF calc_blob_bounds(const text::TextBlobRef& blob)
|
||||||
|
{
|
||||||
|
gfx::RectF bounds = get_text_blob_required_bounds(blob);
|
||||||
|
ui::Paint paint = get_paint_for_text();
|
||||||
|
if (paint.style() == ui::Paint::Style::Stroke ||
|
||||||
|
paint.style() == ui::Paint::Style::StrokeAndFill) {
|
||||||
|
bounds.enlarge(std::ceil(paint.strokeWidth()));
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
class WritingTextState::TextEditor : public Entry {
|
class WritingTextState::TextEditor : public Entry {
|
||||||
public:
|
public:
|
||||||
|
enum TextPreview {
|
||||||
|
Intermediate, // With selection preview / user interface
|
||||||
|
Final, // Final to be rendered in the cel
|
||||||
|
};
|
||||||
|
|
||||||
TextEditor(Editor* editor, const Site& site, const gfx::Rect& bounds)
|
TextEditor(Editor* editor, const Site& site, const gfx::Rect& bounds)
|
||||||
: Entry(4096, "")
|
: Entry(4096, "")
|
||||||
, m_editor(editor)
|
, m_editor(editor)
|
||||||
|
|
@ -61,7 +92,7 @@ public:
|
||||||
setPersistSelection(true);
|
setPersistSelection(true);
|
||||||
|
|
||||||
createExtraCel(site, bounds);
|
createExtraCel(site, bounds);
|
||||||
renderExtraCelBase();
|
renderExtraCel(TextPreview::Intermediate);
|
||||||
|
|
||||||
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
|
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
|
||||||
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
|
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
|
||||||
|
|
@ -76,35 +107,37 @@ public:
|
||||||
|
|
||||||
// Returns the extra cel with the text rendered (but without the
|
// Returns the extra cel with the text rendered (but without the
|
||||||
// selected text highlighted).
|
// selected text highlighted).
|
||||||
ExtraCelRef extraCel()
|
ExtraCelRef extraCel(const TextPreview textPreview)
|
||||||
{
|
{
|
||||||
renderExtraCelBase();
|
renderExtraCel(textPreview);
|
||||||
renderExtraCelText(false);
|
|
||||||
return m_extraCel;
|
return m_extraCel;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setExtraCelBounds(const gfx::Rect& bounds)
|
void setExtraCelBounds(const gfx::RectF& bounds)
|
||||||
{
|
{
|
||||||
if (bounds.w != m_extraCel->image()->width() || bounds.h != m_extraCel->image()->height()) {
|
doc::Image* extraImg = m_extraCel->image();
|
||||||
|
if (!extraImg || std::ceil(bounds.w) != extraImg->width() ||
|
||||||
|
std::ceil(bounds.h) != extraImg->height()) {
|
||||||
createExtraCel(m_editor->getSite(), bounds);
|
createExtraCel(m_editor->getSite(), bounds);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
m_baseBounds = bounds;
|
||||||
m_extraCel->cel()->setBounds(bounds);
|
m_extraCel->cel()->setBounds(bounds);
|
||||||
}
|
}
|
||||||
renderExtraCelBase();
|
renderExtraCel(TextPreview::Intermediate);
|
||||||
renderExtraCelText(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obs::signal<void(const gfx::Size&)> NewRequiredBounds;
|
obs::signal<void(const gfx::RectF&)> NewRequiredBounds;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void createExtraCel(const Site& site, const gfx::Rect& bounds)
|
void createExtraCel(const Site& site, const gfx::Rect& bounds)
|
||||||
{
|
{
|
||||||
|
m_baseBounds = bounds;
|
||||||
m_extraCel->create(ExtraCel::Purpose::TextPreview,
|
m_extraCel->create(ExtraCel::Purpose::TextPreview,
|
||||||
site.tilemapMode(),
|
site.tilemapMode(),
|
||||||
site.sprite(),
|
site.sprite(),
|
||||||
bounds,
|
bounds,
|
||||||
bounds.size(),
|
gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h)),
|
||||||
site.frame(),
|
site.frame(),
|
||||||
255);
|
255);
|
||||||
|
|
||||||
|
|
@ -175,7 +208,7 @@ private:
|
||||||
|
|
||||||
// Notify that we could make the text editor bigger to show this
|
// Notify that we could make the text editor bigger to show this
|
||||||
// text blob.
|
// text blob.
|
||||||
NewRequiredBounds(get_text_blob_required_size(blob));
|
NewRequiredBounds(calc_blob_bounds(blob));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPaint(PaintEvent& ev) override
|
void onPaint(PaintEvent& ev) override
|
||||||
|
|
@ -204,8 +237,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render extra cel with text + selected text
|
// Render extra cel with text + selected text
|
||||||
renderExtraCelBase();
|
renderExtraCel(TextPreview::Intermediate);
|
||||||
renderExtraCelText(true);
|
|
||||||
m_doc->setExtraCel(m_extraCel);
|
m_doc->setExtraCel(m_extraCel);
|
||||||
|
|
||||||
// Paint caret
|
// Paint caret
|
||||||
|
|
@ -226,62 +258,80 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void renderExtraCelBase()
|
void renderExtraCel(const TextPreview textPreview)
|
||||||
{
|
{
|
||||||
auto extraImg = m_extraCel->image();
|
doc::Image* extraImg = m_extraCel->image();
|
||||||
extraImg->clear(extraImg->maskColor());
|
ASSERT(extraImg);
|
||||||
render::Render().renderLayer(extraImg,
|
if (!extraImg)
|
||||||
m_editor->layer(),
|
|
||||||
m_editor->frame(),
|
|
||||||
gfx::Clip(0, 0, m_extraCel->cel()->bounds()),
|
|
||||||
doc::BlendMode::SRC);
|
|
||||||
}
|
|
||||||
|
|
||||||
void renderExtraCelText(const bool withSelection)
|
|
||||||
{
|
|
||||||
const auto textColor = color_utils::color_for_image(Preferences::instance().colorBar.fgColor(),
|
|
||||||
IMAGE_RGB);
|
|
||||||
|
|
||||||
text::TextBlobRef blob = textBlob();
|
|
||||||
if (!blob)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
doc::ImageRef image = render_text_blob(blob, textColor);
|
extraImg->clear(extraImg->maskColor());
|
||||||
if (!image)
|
|
||||||
|
text::TextBlobRef blob = textBlob();
|
||||||
|
doc::ImageRef blobImage;
|
||||||
|
gfx::RectF bounds;
|
||||||
|
if (blob) {
|
||||||
|
const ui::Paint paint = get_paint_for_text();
|
||||||
|
bounds = calc_blob_bounds(blob);
|
||||||
|
blobImage = render_text_blob(blob, bounds, get_paint_for_text());
|
||||||
|
if (!blobImage)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Invert selected range in the image
|
// Invert selected range in the image
|
||||||
if (withSelection) {
|
if (textPreview == TextPreview::Intermediate) {
|
||||||
Range range;
|
Range range;
|
||||||
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
|
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
|
||||||
if (!range.isEmpty()) {
|
if (!range.isEmpty()) {
|
||||||
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
|
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
|
||||||
|
|
||||||
if (!selectedBounds.isEmpty()) {
|
if (!selectedBounds.isEmpty()) {
|
||||||
|
selectedBounds.offset(-bounds.origin());
|
||||||
|
|
||||||
#ifdef LAF_SKIA
|
#ifdef LAF_SKIA
|
||||||
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
|
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(blobImage.get());
|
||||||
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
|
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
|
||||||
|
|
||||||
os::Paint paint;
|
os::Paint paint2 = paint;
|
||||||
paint.blendMode(os::BlendMode::Xor);
|
paint2.blendMode(os::BlendMode::Xor);
|
||||||
paint.color(textColor);
|
paint2.style(os::Paint::Style::Fill);
|
||||||
surface->drawRect(selectedBounds, paint);
|
surface->drawRect(selectedBounds, paint2);
|
||||||
#endif // LAF_SKIA
|
#endif // LAF_SKIA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
doc::blend_image(m_extraCel->image(),
|
doc::Cel* extraCel = m_extraCel->cel();
|
||||||
image.get(),
|
ASSERT(extraCel);
|
||||||
gfx::Clip(image->bounds().size()),
|
if (!extraCel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
extraCel->setPosition(m_baseBounds.x + bounds.x, m_baseBounds.y + bounds.y);
|
||||||
|
|
||||||
|
render::Render().renderLayer(extraImg,
|
||||||
|
m_editor->layer(),
|
||||||
|
m_editor->frame(),
|
||||||
|
gfx::Clip(0, 0, extraCel->bounds()),
|
||||||
|
doc::BlendMode::SRC);
|
||||||
|
|
||||||
|
if (blobImage) {
|
||||||
|
doc::blend_image(extraImg,
|
||||||
|
blobImage.get(),
|
||||||
|
gfx::Clip(blobImage->bounds().size()),
|
||||||
m_doc->sprite()->palette(m_editor->frame()),
|
m_doc->sprite()->palette(m_editor->frame()),
|
||||||
255,
|
255,
|
||||||
doc::BlendMode::NORMAL);
|
doc::BlendMode::NORMAL);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Editor* m_editor;
|
Editor* m_editor;
|
||||||
Doc* m_doc;
|
Doc* m_doc;
|
||||||
ExtraCelRef m_extraCel;
|
ExtraCelRef m_extraCel;
|
||||||
|
|
||||||
|
// Initial bounds for the entry field. This can be modified later to
|
||||||
|
// render the text in case some initial letter/glyph needs some
|
||||||
|
// extra room at the left side.
|
||||||
|
gfx::Rect m_baseBounds;
|
||||||
};
|
};
|
||||||
|
|
||||||
WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
|
WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
|
||||||
|
|
@ -297,10 +347,10 @@ WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
|
||||||
m_fontChangeConn =
|
m_fontChangeConn =
|
||||||
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
|
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
|
||||||
|
|
||||||
m_entry->NewRequiredBounds.connect([this](const gfx::Size& blobSize) {
|
m_entry->NewRequiredBounds.connect([this](const gfx::RectF& blobBounds) {
|
||||||
if (m_bounds.w < blobSize.w || m_bounds.h < blobSize.h) {
|
if (m_bounds.w < blobBounds.w || m_bounds.h < blobBounds.h) {
|
||||||
m_bounds.w = std::max(m_bounds.w, blobSize.w);
|
m_bounds.w = std::max(m_bounds.w, blobBounds.w);
|
||||||
m_bounds.h = std::max(m_bounds.h, blobSize.h);
|
m_bounds.h = std::max(m_bounds.h, blobBounds.h);
|
||||||
m_entry->setExtraCelBounds(m_bounds);
|
m_entry->setExtraCelBounds(m_bounds);
|
||||||
m_entry->setBounds(calcEntryBounds());
|
m_entry->setBounds(calcEntryBounds());
|
||||||
}
|
}
|
||||||
|
|
@ -373,11 +423,11 @@ void WritingTextState::onCommitMouseMove(Editor* editor, const gfx::PointF& spri
|
||||||
if (!m_movingBounds)
|
if (!m_movingBounds)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
gfx::Point delta(spritePos - m_cursorStart);
|
gfx::PointF delta(spritePos - m_cursorStart);
|
||||||
if (delta.x == 0 && delta.y == 0)
|
if (delta.x == 0 && delta.y == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_bounds.setOrigin(gfx::Point(delta + m_boundsOrigin));
|
m_bounds.setOrigin(delta + m_boundsOrigin);
|
||||||
m_entry->setExtraCelBounds(m_bounds);
|
m_entry->setExtraCelBounds(m_bounds);
|
||||||
m_entry->setBounds(calcEntryBounds());
|
m_entry->setBounds(calcEntryBounds());
|
||||||
}
|
}
|
||||||
|
|
@ -393,12 +443,7 @@ bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WritingTextState::onKeyDown(Editor*, KeyMessage*)
|
bool WritingTextState::onKeyDown(Editor*, KeyMessage* msg)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
|
||||||
{
|
{
|
||||||
// Cancel loop pressing Esc key
|
// Cancel loop pressing Esc key
|
||||||
if (msg->scancode() == ui::kKeyEsc) {
|
if (msg->scancode() == ui::kKeyEsc) {
|
||||||
|
|
@ -407,7 +452,17 @@ bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
||||||
// Drop text pressing Enter key
|
// Drop text pressing Enter key
|
||||||
else if (msg->scancode() == ui::kKeyEnter) {
|
else if (msg->scancode() == ui::kKeyEnter) {
|
||||||
drop();
|
drop();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
||||||
|
{
|
||||||
|
// Note: We cannot process kKeyEnter key here to drop the text as it
|
||||||
|
// could be received after the Enter key is pressed in the IME
|
||||||
|
// dialog to accept the composition (not to accept the text). So we
|
||||||
|
// process kKeyEnter in onKeyDown().
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,8 +515,8 @@ EditorState::LeaveAction WritingTextState::onLeaveState(Editor* editor, EditorSt
|
||||||
// Paints the text in the active layer/sprite creating an
|
// Paints the text in the active layer/sprite creating an
|
||||||
// undoable transaction.
|
// undoable transaction.
|
||||||
Site site = m_editor->getSite();
|
Site site = m_editor->getSite();
|
||||||
ExtraCelRef extraCel = m_entry->extraCel();
|
ExtraCelRef extraCel = m_entry->extraCel(TextEditor::Final);
|
||||||
Tx tx(site.document(), "Text Tool");
|
Tx tx(site.document(), Strings::tools_text());
|
||||||
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
|
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
|
||||||
|
|
||||||
expand.validateDestCanvas(gfx::Region(extraCel->cel()->bounds()));
|
expand.validateDestCanvas(gfx::Region(extraCel->cel()->bounds()));
|
||||||
|
|
@ -512,7 +567,7 @@ void WritingTextState::onFontChange(const FontInfo& fontInfo, FontEntry::From fr
|
||||||
|
|
||||||
// This is useful to show changes to the anti-alias option
|
// This is useful to show changes to the anti-alias option
|
||||||
// immediately.
|
// immediately.
|
||||||
auto dummy = m_entry->extraCel();
|
auto dummy = m_entry->extraCel(TextEditor::Intermediate);
|
||||||
|
|
||||||
if (fromField == FontEntry::From::Popup) {
|
if (fromField == FontEntry::From::Popup) {
|
||||||
if (m_entry)
|
if (m_entry)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (c) 2022-2024 Igara Studio S.A.
|
// Copyright (c) 2022-2025 Igara Studio S.A.
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
@ -59,7 +59,7 @@ private:
|
||||||
|
|
||||||
DelayedMouseMove m_delayedMouseMove;
|
DelayedMouseMove m_delayedMouseMove;
|
||||||
Editor* m_editor;
|
Editor* m_editor;
|
||||||
gfx::Rect m_bounds;
|
gfx::RectF m_bounds;
|
||||||
std::unique_ptr<TextEditor> m_entry;
|
std::unique_ptr<TextEditor> m_entry;
|
||||||
|
|
||||||
// True if the text was discarded.
|
// True if the text was discarded.
|
||||||
|
|
@ -71,7 +71,7 @@ private:
|
||||||
bool m_mouseMoveReceived = false;
|
bool m_mouseMoveReceived = false;
|
||||||
bool m_movingBounds = false;
|
bool m_movingBounds = false;
|
||||||
gfx::PointF m_cursorStart;
|
gfx::PointF m_cursorStart;
|
||||||
gfx::Point m_boundsOrigin;
|
gfx::PointF m_boundsOrigin;
|
||||||
|
|
||||||
obs::scoped_connection m_beforeCmdConn;
|
obs::scoped_connection m_beforeCmdConn;
|
||||||
obs::scoped_connection m_fontChangeConn;
|
obs::scoped_connection m_fontChangeConn;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2018 David Capello
|
// Copyright (C) 2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -46,7 +46,7 @@ ExportFileWindow::ExportFileWindow(const Doc* doc)
|
||||||
base = base::join_path(basePath, base::get_file_title(base));
|
base = base::join_path(basePath, base::get_file_title(base));
|
||||||
|
|
||||||
std::string newFn = base::replace_extension(base, defaultExtension());
|
std::string newFn = base::replace_extension(base, defaultExtension());
|
||||||
if (newFn == base) {
|
if (newFn == base::replace_extension(base, base::get_file_extension(doc->filename()))) {
|
||||||
newFn = base::join_path(
|
newFn = base::join_path(
|
||||||
base::get_file_path(newFn),
|
base::get_file_path(newFn),
|
||||||
base::get_file_title(newFn) + "-export." + base::get_file_extension(newFn));
|
base::get_file_title(newFn) + "-export." + base::get_file_extension(newFn));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
#include "base/time.h"
|
#include "base/time.h"
|
||||||
#include "os/surface.h"
|
#include "os/surface.h"
|
||||||
#include "text/font.h"
|
#include "text/font.h"
|
||||||
|
#include "text/font_metrics.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -44,6 +45,7 @@ FileList::FileList()
|
||||||
, m_multiselect(false)
|
, m_multiselect(false)
|
||||||
, m_zoom(1.0)
|
, m_zoom(1.0)
|
||||||
, m_itemsPerRow(0)
|
, m_itemsPerRow(0)
|
||||||
|
, m_showHidden(Preferences::instance().fileSelector.showHidden())
|
||||||
{
|
{
|
||||||
setFocusStop(true);
|
setFocusStop(true);
|
||||||
setDoubleBuffered(true);
|
setDoubleBuffered(true);
|
||||||
|
|
@ -172,6 +174,14 @@ void FileList::animateToZoom(const double zoom)
|
||||||
startAnimation(ANI_ZOOM, 10);
|
startAnimation(ANI_ZOOM, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FileList::setShowHidden(const bool show)
|
||||||
|
{
|
||||||
|
m_showHidden = show;
|
||||||
|
m_req_valid = false;
|
||||||
|
m_selected = nullptr;
|
||||||
|
regenerateList();
|
||||||
|
}
|
||||||
|
|
||||||
bool FileList::onProcessMessage(Message* msg)
|
bool FileList::onProcessMessage(Message* msg)
|
||||||
{
|
{
|
||||||
switch (msg->type()) {
|
switch (msg->type()) {
|
||||||
|
|
@ -401,7 +411,6 @@ void FileList::onPaint(ui::PaintEvent& ev)
|
||||||
gfx::Rect bounds = clientBounds();
|
gfx::Rect bounds = clientBounds();
|
||||||
|
|
||||||
g->fillRect(theme->colors.background(), bounds);
|
g->fillRect(theme->colors.background(), bounds);
|
||||||
// g->fillRect(bgcolor, gfx::Rect(bounds.x, y, bounds.w, itemSize.h));
|
|
||||||
|
|
||||||
int i = 0, selectedIndex = -1;
|
int i = 0, selectedIndex = -1;
|
||||||
for (IFileItem* fi : m_list) {
|
for (IFileItem* fi : m_list) {
|
||||||
|
|
@ -506,17 +515,28 @@ void FileList::paintItem(ui::Graphics* g, IFileItem* fi, const int i)
|
||||||
|
|
||||||
// item name
|
// item name
|
||||||
if (isIconView() && textBounds.w > info.bounds.w) {
|
if (isIconView() && textBounds.w > info.bounds.w) {
|
||||||
g->drawAlignedUIText(fi->displayName().c_str(),
|
g->drawAlignedUIText(fi->displayName(),
|
||||||
fgcolor,
|
fgcolor,
|
||||||
bgcolor,
|
bgcolor,
|
||||||
(textBounds & gfx::Rect(info.bounds).shrink(2 * guiscale())),
|
(textBounds & gfx::Rect(info.bounds).shrink(2 * guiscale())),
|
||||||
ui::CENTER | ui::TOP | ui::CHARWRAP);
|
ui::CENTER | ui::TOP | ui::CHARWRAP);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
g->drawText(fi->displayName().c_str(),
|
auto blob = text::TextBlob::MakeWithShaper(theme->fontMgr(), font(), fi->displayName());
|
||||||
fgcolor,
|
if (blob) {
|
||||||
bgcolor,
|
Paint paint;
|
||||||
gfx::Point(textBounds.x + 2 * guiscale(), textBounds.y + 2 * guiscale()));
|
paint.color(fgcolor);
|
||||||
|
paint.style(os::Paint::Fill);
|
||||||
|
|
||||||
|
text::FontMetrics metrics;
|
||||||
|
font()->metrics(&metrics);
|
||||||
|
const float baselineDelta = -metrics.ascent - blob->baseline();
|
||||||
|
|
||||||
|
g->drawTextBlob(
|
||||||
|
blob,
|
||||||
|
gfx::PointF(textBounds.x + 2 * guiscale(), textBounds.y + 2 * guiscale() + baselineDelta),
|
||||||
|
paint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw thumbnail progress bar
|
// Draw thumbnail progress bar
|
||||||
|
|
@ -814,7 +834,7 @@ void FileList::regenerateList()
|
||||||
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
|
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
|
||||||
IFileItem* fileitem = *it;
|
IFileItem* fileitem = *it;
|
||||||
|
|
||||||
if (fileitem->isHidden())
|
if (fileitem->isHidden() && !m_showHidden)
|
||||||
it = m_list.erase(it);
|
it = m_list.erase(it);
|
||||||
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
|
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
|
||||||
it = m_list.erase(it);
|
it = m_list.erase(it);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ public:
|
||||||
double zoom() const { return m_zoom; }
|
double zoom() const { return m_zoom; }
|
||||||
void setZoom(const double zoom);
|
void setZoom(const double zoom);
|
||||||
void animateToZoom(const double zoom);
|
void animateToZoom(const double zoom);
|
||||||
|
void setShowHidden(const bool show);
|
||||||
|
|
||||||
obs::signal<void()> FileSelected;
|
obs::signal<void()> FileSelected;
|
||||||
obs::signal<void()> FileAccepted;
|
obs::signal<void()> FileAccepted;
|
||||||
|
|
@ -137,6 +138,7 @@ private:
|
||||||
double m_toZoom;
|
double m_toZoom;
|
||||||
|
|
||||||
int m_itemsPerRow;
|
int m_itemsPerRow;
|
||||||
|
bool m_showHidden;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
|
||||||
for (auto child : viewType()->children())
|
for (auto child : viewType()->children())
|
||||||
child->setFocusStop(false);
|
child->setFocusStop(false);
|
||||||
|
|
||||||
|
showHiddenCheck()->setSelected(Preferences::instance().fileSelector.showHidden());
|
||||||
m_fileList = new FileList();
|
m_fileList = new FileList();
|
||||||
m_fileList->setId("fileview");
|
m_fileList->setId("fileview");
|
||||||
m_fileName->setAssociatedFileList(m_fileList);
|
m_fileName->setAssociatedFileList(m_fileList);
|
||||||
|
|
@ -334,6 +335,10 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
|
||||||
viewType()->ItemChange.connect([this] { onChangeViewType(); });
|
viewType()->ItemChange.connect([this] { onChangeViewType(); });
|
||||||
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
|
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
|
||||||
fileType()->Change.connect([this] { onFileTypeChange(); });
|
fileType()->Change.connect([this] { onFileTypeChange(); });
|
||||||
|
showHiddenCheck()->Click.connect([this] {
|
||||||
|
Preferences::instance().fileSelector.showHidden(showHiddenCheck()->isSelected());
|
||||||
|
m_fileList->setShowHidden(showHiddenCheck()->isSelected());
|
||||||
|
});
|
||||||
m_fileList->FileSelected.connect([this] { onFileListFileSelected(); });
|
m_fileList->FileSelected.connect([this] { onFileListFileSelected(); });
|
||||||
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
|
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
|
||||||
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });
|
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });
|
||||||
|
|
@ -408,7 +413,8 @@ bool FileSelector::show(const std::string& title,
|
||||||
|
|
||||||
remapWindow();
|
remapWindow();
|
||||||
centerWindow();
|
centerWindow();
|
||||||
load_window_pos(this, kConfigSection);
|
// The minimum size is large, so do not limit the minimum loaded size
|
||||||
|
load_window_pos(this, kConfigSection, false);
|
||||||
|
|
||||||
// Change the file formats/extensions to be shown
|
// Change the file formats/extensions to be shown
|
||||||
std::string initialExtension = base::get_file_extension(initialPath);
|
std::string initialExtension = base::get_file_extension(initialPath);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue