Compare commits
85 Commits
v1.3.14-be
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
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 | |
|
|
41a8249afd | |
|
|
5dae7e203f | |
|
|
d8e8074345 | |
|
|
95d65d8163 | |
|
|
a8e190a133 | |
|
|
7c74619c94 | |
|
|
5678086310 | |
|
|
8158345cea | |
|
|
06d3bbf953 | |
|
|
6aabfef0b8 | |
|
|
206065fb80 | |
|
|
90be6aac30 | |
|
|
600882352e | |
|
|
aca46a28c5 | |
|
|
b099d8f780 | |
|
|
378f7ac6c2 | |
|
|
3bc84cca53 | |
|
|
3549d3538f | |
|
|
39d69ac8cf | |
|
|
ba5adcaa7d | |
|
|
9e35fd817a | |
|
|
65c2ed6a35 | |
|
|
c59f1825be | |
|
|
7f07becd74 | |
|
|
fdc9b2f000 | |
|
|
2e37ac9b83 | |
|
|
753d892af2 | |
|
|
8a8ddbc630 | |
|
|
943c3b28df | |
|
|
d9a138357e | |
|
|
978000a9dd | |
|
|
b3f4e37b69 | |
|
|
d8632b6208 | |
|
|
3c4d012210 | |
|
|
d51a6d4f51 | |
|
|
2c9eb2a801 | |
|
|
bae8520580 | |
|
|
7167969963 | |
|
|
537ccd393f | |
|
|
b130601716 | |
|
|
816be744ac | |
|
|
50bccf85fc | |
|
|
2ee4819bf9 | |
|
|
805d98e943 | |
|
|
c68b4923f8 | |
|
|
53c415c933 | |
|
|
d3265a1711 | |
|
|
c949f4a5a6 | |
|
|
2706d2d75a | |
|
|
ce989684d0 | |
|
|
da5909639b | |
|
|
fc63532fef |
|
|
@ -60,6 +60,8 @@ Checks: >
|
|||
-readability-uppercase-literal-suffix
|
||||
WarningsAsErrors: ''
|
||||
CheckOptions:
|
||||
- key: readability-implicit-bool-conversion.AllowIntegerConditions
|
||||
value: true
|
||||
- key: readability-implicit-bool-conversion.AllowPointerConditions
|
||||
value: true
|
||||
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
name: build-auto
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/build-auto.yml'
|
||||
- 'build.sh'
|
||||
- 'laf'
|
||||
jobs:
|
||||
build-auto:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
build_type: [RelWithDebInfo, Debug]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: Install Dependencies
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y \
|
||||
libpixman-1-dev libfreetype6-dev libharfbuzz-dev zlib1g-dev \
|
||||
libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
|
||||
- uses: aseprite/get-ninja@main
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
if: runner.os == 'Windows'
|
||||
- name: Building
|
||||
shell: bash
|
||||
run: |
|
||||
bash build.sh --auto --norun
|
||||
- name: Running CLI Tests
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ runner.os }}" == "Linux" ]] ; then
|
||||
export XVFB=xvfb-run
|
||||
fi
|
||||
export ASEPRITE=$PWD/build/bin/aseprite
|
||||
cd tests
|
||||
$XVFB bash run-tests.sh
|
||||
|
|
@ -34,16 +34,14 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ runner.os }}" == "Windows" ]] ; then
|
||||
choco install wget -y --no-progress
|
||||
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-Windows-Release-x64.zip
|
||||
unzip Skia-Windows-Release-x64.zip -d skia
|
||||
elif [[ "${{ runner.os }}" == "macOS" ]] ; then
|
||||
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-macOS-Release-arm64.zip
|
||||
unzip Skia-macOS-Release-arm64.zip -d skia
|
||||
this_dir=$(cygpath "${{ github.workspace }}")
|
||||
else
|
||||
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-Linux-Release-x64.zip
|
||||
unzip Skia-Linux-Release-x64.zip -d skia
|
||||
this_dir="${{ github.workspace }}"
|
||||
fi
|
||||
skia_url=$(source $this_dir/laf/misc/skia-url.sh | xargs)
|
||||
skia_file=$(basename $skia_url)
|
||||
curl --ssl-revoke-best-effort -L -o "$skia_file" "$skia_url"
|
||||
unzip "$skia_file" -d skia
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.17
|
||||
if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ jobs:
|
|||
post-comments:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ZedThree/clang-tidy-review/post@v0.20.1
|
||||
- uses: ZedThree/clang-tidy-review/post@v0.21.0
|
||||
with:
|
||||
token: ${{ secrets.CLANG_TIDY_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@
|
|||
*.res
|
||||
.DS_Store
|
||||
.vs
|
||||
.vscode
|
||||
tests/_test*
|
||||
build
|
||||
.build
|
||||
.deps
|
||||
CMakeSettings.json
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
# Credits
|
||||
|
||||
Aseprite is being developed and maintained currently by [Igara Studio](https://igara.com/).
|
||||
The active team of developers is:
|
||||
|
||||
* [David Capello](https://github.com/dacap)
|
||||
* [Gaspar Capello](https://github.com/Gsparoken)
|
||||
* [Martín Capello](https://github.com/martincapello)
|
||||
* [Christian Kaiser](https://github.com/ckaiser)
|
||||
* [Dante Paola](https://github.com/Liebranca)
|
||||
|
||||
Previous team members that contributed with code/docs/scripts/graphics:
|
||||
|
||||
* [Kacper Woźniak](https://github.com/thkwznk)
|
||||
* [Joshua Ogunyinka](https://github.com/iamOgunyinka)
|
||||
* [David Campo](https://github.com/dncampo)
|
||||
|
||||
## Translations
|
||||
|
||||
The translation work of Aseprite is only possible thanks to the
|
||||
contribution, help, and good will of several translators coordinated
|
||||
through our Weblate project:
|
||||
|
||||
* [Translation Credits](strings/README.md)
|
||||
|
||||
## Graphics
|
||||
|
||||
Aseprite logo was created by David Capello. Graphics used as background
|
||||
of [Aseprite home page](https://www.aseprite.org),
|
||||
on [Steam Store](https://store.steampowered.com/app/431730/Aseprite/),
|
||||
and [social media channels](https://bsky.app/profile/aseprite.org),
|
||||
were created by:
|
||||
|
||||
* [Ilija Melentijevic](https://ilkke.net/)
|
||||
|
||||
## Themes
|
||||
|
||||
The default Aseprite font was created by David Capello, and the
|
||||
default Aseprite theme was introduced in v0.8, originally created by:
|
||||
|
||||
* Ilija Melentijevic
|
||||
|
||||
A modified dark version of this theme was introduced in v1.3-beta1, created by:
|
||||
|
||||
* [Nicolas Desilets](https://twitter.com/MapleGecko)
|
||||
|
||||
These themes are now being maintained by Igara Studio and external
|
||||
contributors from time to time.
|
||||
|
||||
## Palettes
|
||||
|
||||
Aseprite includes color palettes created by:
|
||||
|
||||
* [Richard "DawnBringer" Fhager](http://pixeljoint.com/p/23821.htm), [DB16](http://pixeljoint.com/forum/forum_posts.asp?TID=12795), [DB32](http://pixeljoint.com/forum/forum_posts.asp?TID=16247) (default Aseprite color palette)
|
||||
* [Arne Niklas Jansson](http://androidarts.com/), [16 colors](http://androidarts.com/palette/16pal.htm), [32 colors](http://wayofthepixel.net/index.php?topic=15824.msg144494)
|
||||
* [ENDESGA Studios](https://twitter.com/ENDESGA), [EDG16 and EDG32](https://forums.tigsource.com/index.php?topic=46126.msg1279124#msg1279124), and [other palettes](https://twitter.com/ENDESGA/status/865812366931353600)
|
||||
* [Hyohnoo Games](https://twitter.com/Hyohnoo), [mail24](https://twitter.com/Hyohnoo/status/797472587974639616) palette
|
||||
* [Davit Masia](https://twitter.com/DavitMasia), [matriax8c](https://twitter.com/DavitMasia/status/834862452164612096) palette
|
||||
* [Javier Guerrero](https://twitter.com/Xavier_Gd), [nyx8](https://twitter.com/Xavier_Gd/status/868519467864686594) palette
|
||||
* [Adigun A. Polack](https://twitter.com/adigunpolack), [AAP-64](http://pixeljoint.com/pixelart/119466.htm), [AAP-Splendor128](http://pixeljoint.com/pixelart/120714.htm), [SimpleJPC-16](http://pixeljoint.com/pixelart/119844.htm), and [AAP-Micro12](http://pixeljoint.com/pixelart/121151.htm) palette
|
||||
* [PineTreePizza](https://twitter.com/PineTreePizza), [Rosy-42](https://twitter.com/PineTreePizza/status/1006536191955623938) palette
|
||||
|
||||
## Pixel-art Features
|
||||
|
||||
Aseprite tries to replicate some pixel-art algorithms:
|
||||
|
||||
* [Shading Ink](https://aseprite.org/docs/shading/): created as a simplification of GrafX2 shade mode, thanks to Ilija Melentijevic for introducing me to this feature in 2009
|
||||
* [RotSprite](http://forums.sonicretro.org/index.php?showtopic=8848&st=15&p=159754&#entry159754) by Xenowhirl.
|
||||
* [Pixel perfect drawing algorithm](https://deepnight.net/blog/tools/pixel-perfect-drawing/)
|
||||
by [Sébastien Bénard](https://twitter.com/deepnightfr) and
|
||||
[Carduus](https://twitter.com/CarduusHimself/status/420554200737935361).
|
||||
|
||||
## Community
|
||||
|
||||
A special thanks to @Outlander for helping us moderating our [Discord server](https://discord.gg/Yb2CeX8).
|
||||
Thanks to all the people that hung around for such a long time.
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you everyone who contributed to Aseprite with ideas, patches,
|
||||
code, bug reports, new features, donations, tutorials, videos,
|
||||
personal messages, chats, emails, tweets, posts, questions, libraries,
|
||||
compilers, and any other tools that made this program possible today.
|
||||
|
||||
* Thanks to all [contributors](https://github.com/aseprite/aseprite/graphs/contributors)
|
||||
* Thanks to all developers and maintainers behind [other open source projects](docs/LICENSES.md) used by Aseprite
|
||||
* Thanks to all early PayPal donors and donors from our Pledgie Campaign (before Aseprite was commercialized)
|
||||
* Thanks to every who support our business model: this source-available / sell-binaries combo
|
||||
* Thanks to schools and [educational institutions](https://aseprite.org/educational)
|
||||
that are using Aseprite in their classrooms <3
|
||||
* Thanks to our family and friends who always support our work
|
||||
|
||||
It's been more years than I can remember, sorry if we missed someone,
|
||||
please drop me a line to [david@igara.com](mailto:david@igara.com) to
|
||||
fix something or say hi. We'll try to keep this updated (for past and
|
||||
future contributors).
|
||||
|
||||
Sincerely, David.
|
||||
|
|
@ -243,7 +243,13 @@ if(USE_SHARED_LIBPNG)
|
|||
add_definitions(${PNG_DEFINITIONS})
|
||||
else()
|
||||
set(PNG_FOUND ON)
|
||||
set(PNG_LIBRARY png_static)
|
||||
|
||||
# Skia on Linux includes libpng symbols
|
||||
if(UNIX AND NOT APPLE AND LAF_BACKEND STREQUAL "skia")
|
||||
set(PNG_LIBRARY skia)
|
||||
else()
|
||||
set(PNG_LIBRARY png_static)
|
||||
endif()
|
||||
set(PNG_LIBRARIES ${PNG_LIBRARY})
|
||||
set(PNG_INCLUDE_DIRS
|
||||
${LIBPNG_DIR}
|
||||
|
|
|
|||
61
INSTALL.md
61
INSTALL.md
|
|
@ -6,7 +6,8 @@
|
|||
* [Windows dependencies](#windows-dependencies)
|
||||
* [macOS dependencies](#macos-dependencies)
|
||||
* [Linux dependencies](#linux-dependencies)
|
||||
* [Compiling](#compiling)
|
||||
* [Automatic Building](#automatic-building)
|
||||
* [Manual Building](#manual-building)
|
||||
* [Windows details](#windows-details)
|
||||
* [MinGW](#mingw)
|
||||
* [macOS details](#macos-details)
|
||||
|
|
@ -17,11 +18,12 @@
|
|||
# Platforms
|
||||
|
||||
You should be able to compile Aseprite successfully on the following
|
||||
platforms:
|
||||
platforms (older and newer versions might work):
|
||||
|
||||
* Windows 11 + [Visual Studio Community 2022 + Windows 10.0 SDK (the latest version available)](https://imgur.com/a/7zs51IT) (we don't support [MinGW](#mingw))
|
||||
* macOS 13.0.1 Ventura + Xcode 14.1 + macOS 11.3 SDK (older version might work)
|
||||
* Linux Ubuntu Bionic 18.04 + clang 10.0
|
||||
* Windows 11 + [Visual Studio Community 2022 + Windows 11 SDK](https://imgur.com/a/7zs51IT)
|
||||
* *Important*: We don't support [MinGW](#mingw)
|
||||
* macOS 15.2 Sequoia + Xcode 16.3 + macOS 15.4 SDK
|
||||
* Linux Ubuntu Focal Fossa 20.04 + clang 12
|
||||
|
||||
# Get the source code
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ clone the repository on Windows.
|
|||
|
||||
To compile Aseprite you will need:
|
||||
|
||||
* The latest version of [CMake](https://cmake.org) (3.16 or greater)
|
||||
* The latest version of [CMake](https://cmake.org)
|
||||
* [Ninja](https://ninja-build.org) build system
|
||||
* And a compiled version of the `aseprite-m124` branch of
|
||||
the [Skia library](https://github.com/aseprite/skia#readme).
|
||||
|
|
@ -59,25 +61,24 @@ To compile Aseprite you will need:
|
|||
|
||||
## Windows dependencies
|
||||
|
||||
* Windows 10/11 (we don't support cross-compiling)
|
||||
* Windows 11 (we don't support cross-compiling)
|
||||
* [Visual Studio Community 2022](https://visualstudio.microsoft.com/downloads/) (we don't support [MinGW](#mingw))
|
||||
* The [Desktop development with C++ item + Windows 10.0.18362.0 SDK](https://imgur.com/a/7zs51IT)
|
||||
from the Visual Studio installer
|
||||
* The [Desktop development with C++ item + Windows 10.0.26100.0 SDK](https://imgur.com/a/7zs51IT)
|
||||
from Visual Studio installer
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
On macOS you will need macOS 11.3 SDK and Xcode 13.1 (older versions
|
||||
might work).
|
||||
On macOS you will need macOS 15.4 SDK and Xcode 16.3 (older versions might work).
|
||||
|
||||
## Linux dependencies
|
||||
|
||||
You will need the following dependencies on Ubuntu/Debian:
|
||||
|
||||
sudo apt-get install -y g++ clang libc++-dev libc++abi-dev cmake ninja-build libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
|
||||
sudo apt-get install -y g++ clang cmake ninja-build libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
|
||||
|
||||
Or use clang-10 packages (or newer) in case that clang in your distribution is older than clang 10.0:
|
||||
Or use clang-12 packages (or newer) in case that clang in your distribution is older than clang 12.0:
|
||||
|
||||
sudo apt-get install -y clang-10 libc++-10-dev libc++abi-10-dev
|
||||
sudo apt-get install -y clang-12
|
||||
|
||||
On Fedora:
|
||||
|
||||
|
|
@ -85,13 +86,24 @@ On Fedora:
|
|||
|
||||
On Arch:
|
||||
|
||||
sudo pacman -S gcc clang libc++ cmake ninja libx11 libxcursor mesa-libgl fontconfig libwebp
|
||||
sudo pacman -S gcc clang cmake ninja libx11 libxcursor mesa-libgl fontconfig libwebp
|
||||
|
||||
On SUSE:
|
||||
|
||||
sudo zypper install gcc-c++ clang libc++-devel libc++abi-devel cmake ninja libX11-devel libXcursor-devel libXi-devel Mesa-libGL-devel fontconfig-devel
|
||||
sudo zypper install gcc-c++ clang cmake ninja libX11-devel libXcursor-devel libXi-devel Mesa-libGL-devel fontconfig-devel
|
||||
|
||||
# Compiling
|
||||
# Automatic Building
|
||||
|
||||
We offer a new [build script](build.sh) that automates and help you to
|
||||
compile Aseprite following instructions on screen. This will be the
|
||||
preferred method for new users and developers to compile Aseprite.
|
||||
|
||||
After you get [get Aseprite code](#get-the-source-code) and install
|
||||
[its dependencies](#dependencies), you can run [build.cmd](build.cmd)
|
||||
file on Windows double-clicking it, or [build.sh](build.sh) on macOS or
|
||||
Linux running it from the terminal from the same Aseprite folder.
|
||||
|
||||
# Manual Building
|
||||
|
||||
1. [Get Aseprite code](#get-the-source-code), put it in a folder like
|
||||
`C:\aseprite`, and create a `build` directory inside to leave all
|
||||
|
|
@ -223,7 +235,9 @@ If you have a Retina display, check the following issue:
|
|||
|
||||
## Linux details
|
||||
|
||||
You need to use clang and libc++ to compile Aseprite:
|
||||
You can compile Aseprite with gcc or clang. In case that you are using
|
||||
the [pre-compiled Skia version](https://github.com/aseprite/skia/releases/),
|
||||
you must use libstdc++ to compile Aseprite:
|
||||
|
||||
cd aseprite
|
||||
mkdir build
|
||||
|
|
@ -232,8 +246,8 @@ You need to use clang and libc++ to compile Aseprite:
|
|||
export CXX=clang++
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_CXX_FLAGS:STRING=-stdlib=libc++ \
|
||||
-DCMAKE_EXE_LINKER_FLAGS:STRING=-stdlib=libc++ \
|
||||
-DCMAKE_CXX_FLAGS:STRING=-stdlib=libstdc++ \
|
||||
-DCMAKE_EXE_LINKER_FLAGS:STRING=-stdlib=libstdc++ \
|
||||
-DLAF_BACKEND=skia \
|
||||
-DSKIA_DIR=$HOME/deps/skia \
|
||||
-DSKIA_LIBRARY_DIR=$HOME/deps/skia/out/Release-x64 \
|
||||
|
|
@ -245,13 +259,6 @@ You need to use clang and libc++ to compile Aseprite:
|
|||
In this case, `$HOME/deps/skia` is the directory where Skia was
|
||||
compiled or uncompressed.
|
||||
|
||||
### GCC compiler
|
||||
|
||||
In case that you are using the pre-compiled Skia version, you must use
|
||||
the clang compiler and libc++ to compile Aseprite. Only if you compile
|
||||
Skia with GCC, you will be able to compile Aseprite with GCC, and this
|
||||
is not recommended as you will have a performance penalty doing so.
|
||||
|
||||
# Using shared third party libraries
|
||||
|
||||
If you don't want to use the embedded code of third party libraries
|
||||
|
|
|
|||
41
README.md
41
README.md
|
|
@ -48,45 +48,14 @@ You can ask for help in:
|
|||
[YouTube](https://www.youtube.com/user/aseprite),
|
||||
[Instagram](https://www.instagram.com/aseprite/).
|
||||
|
||||
## Authors
|
||||
|
||||
Aseprite is being developed by [Igara Studio](https://igara.com/):
|
||||
|
||||
* [David Capello](https://davidcapello.com/)
|
||||
* [Gaspar Capello](https://github.com/Gasparoken)
|
||||
* [Martín Capello](https://github.com/martincapello)
|
||||
|
||||
## Credits
|
||||
|
||||
The default Aseprite theme was introduced in v0.8, created by:
|
||||
Aseprite was originally created by [David Capello](https://davidcapello.com/)
|
||||
and is now being developed and maintained by [Igara Studio](https://igara.com/)
|
||||
and contributors.
|
||||
|
||||
* [Ilija Melentijevic](https://ilkke.net/)
|
||||
|
||||
A modified dark version of this theme introduced in v1.3-beta1 was created by:
|
||||
|
||||
* [Nicolas Desilets](https://twitter.com/MapleGecko)
|
||||
* [David Capello](https://twitter.com/davidcapello)
|
||||
|
||||
Aseprite includes color palettes created by:
|
||||
|
||||
* [Richard "DawnBringer" Fhager](http://pixeljoint.com/p/23821.htm), [16 colors](http://pixeljoint.com/forum/forum_posts.asp?TID=12795), [32 colors](http://pixeljoint.com/forum/forum_posts.asp?TID=16247).
|
||||
* [Arne Niklas Jansson](http://androidarts.com/), [16 colors](http://androidarts.com/palette/16pal.htm), [32 colors](http://wayofthepixel.net/index.php?topic=15824.msg144494).
|
||||
* [ENDESGA Studios](https://twitter.com/ENDESGA), [EDG16 and EDG32](https://forums.tigsource.com/index.php?topic=46126.msg1279124#msg1279124), and [other palettes](https://twitter.com/ENDESGA/status/865812366931353600).
|
||||
* [Hyohnoo Games](https://twitter.com/Hyohnoo), [mail24](https://twitter.com/Hyohnoo/status/797472587974639616) palette.
|
||||
* [Davit Masia](https://twitter.com/DavitMasia), [matriax8c](https://twitter.com/DavitMasia/status/834862452164612096) palette.
|
||||
* [Javier Guerrero](https://twitter.com/Xavier_Gd), [nyx8](https://twitter.com/Xavier_Gd/status/868519467864686594) palette.
|
||||
* [Adigun A. Polack](https://twitter.com/adigunpolack), [AAP-64](http://pixeljoint.com/pixelart/119466.htm), [AAP-Splendor128](http://pixeljoint.com/pixelart/120714.htm), [SimpleJPC-16](http://pixeljoint.com/pixelart/119844.htm), and [AAP-Micro12](http://pixeljoint.com/pixelart/121151.htm) palette.
|
||||
* [PineTreePizza](https://twitter.com/PineTreePizza), [Rosy-42](https://twitter.com/PineTreePizza/status/1006536191955623938) palette.
|
||||
|
||||
It tries to replicate some pixel-art algorithms:
|
||||
|
||||
* [RotSprite](http://forums.sonicretro.org/index.php?showtopic=8848&st=15&p=159754&#entry159754) by Xenowhirl.
|
||||
* [Pixel perfect drawing algorithm](https://deepnight.net/blog/tools/pixel-perfect-drawing/) by [Sébastien Bénard](https://twitter.com/deepnightfr) and [Carduus](https://twitter.com/CarduusHimself/status/420554200737935361).
|
||||
|
||||
Thanks to [third-party open source projects](docs/LICENSES.md), to
|
||||
[contributors](https://www.aseprite.org/contributors/), and all the
|
||||
people who have contributed ideas, patches, bugs report, feature
|
||||
requests, donations, and help us to develop Aseprite.
|
||||
Check the [AUTHORS](AUTHORS.md) file for details about the active team
|
||||
of developers working on Aseprite.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
133
build.sh
133
build.sh
|
|
@ -56,31 +56,6 @@ if [ "$1" == "--norun" ] ; then
|
|||
norun=1
|
||||
fi
|
||||
|
||||
# Platform.
|
||||
if [[ "$(uname)" =~ "MINGW32" ]] || [[ "$(uname)" =~ "MINGW64" ]] || [[ "$(uname)" =~ "MSYS_NT-10.0" ]] ; then
|
||||
is_win=1
|
||||
cpu=x64
|
||||
|
||||
if ! cl.exe >/dev/null 2>/dev/null ; then
|
||||
echo ""
|
||||
echo "MSVC compiler (cl.exe) not found in PATH"
|
||||
echo ""
|
||||
echo " PATH=$PATH"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$(uname)" == "Linux" ]] ; then
|
||||
is_linux=1
|
||||
cpu=x64
|
||||
elif [[ "$(uname)" =~ "Darwin" ]] ; then
|
||||
is_macos=1
|
||||
if [[ $(uname -m) == "arm64" ]]; then
|
||||
cpu=arm64
|
||||
else
|
||||
cpu=x64
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check utilities.
|
||||
if ! cmake --version >/dev/null ; then
|
||||
echo ""
|
||||
|
|
@ -137,6 +112,23 @@ if [ $run_submodule_update ] ; then
|
|||
echo "Done"
|
||||
fi
|
||||
|
||||
# Platform.
|
||||
if ! source "$pwd/laf/misc/platform.sh" ; then
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ $is_win ] ; then
|
||||
# Check MSVC compiler.
|
||||
if ! cl.exe >/dev/null 2>/dev/null ; then
|
||||
echo ""
|
||||
echo "MSVC compiler (cl.exe) not found in PATH"
|
||||
echo ""
|
||||
echo " PATH=$PATH"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create the directory to store the configuration.
|
||||
if [ ! -d "$pwd/.build" ] ; then
|
||||
mkdir "$pwd/.build"
|
||||
|
|
@ -150,25 +142,25 @@ if [ ! -f "$pwd/.build/userkind" ] ; then
|
|||
echo "user" > $pwd/.build/userkind
|
||||
else
|
||||
echo ""
|
||||
echo "Select what kind of user you are (press U or D keys):"
|
||||
echo "Select what kind of user you are (press U or D key and then Enter):"
|
||||
echo ""
|
||||
echo " [U]ser: give a try to Aseprite"
|
||||
echo " [D]eveloper: develop/modify Aseprite"
|
||||
echo ""
|
||||
read -sN 1 -p "[U/D]? "
|
||||
echo ""
|
||||
if [[ "$REPLY" == "d" || "$REPLY" == "D" ]] ; then
|
||||
read -p "[U/D]? "
|
||||
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$REPLY" == "d" || "$REPLY" == "dev" || "$REPLY" == "developer" ]] ; then
|
||||
echo "developer" > $pwd/.build/userkind
|
||||
elif [[ "$REPLY" == "u" || "$REPLY" == "U" ]] ; then
|
||||
elif [[ "$REPLY" == "u" || "$REPLY" == "user" ]] ; then
|
||||
echo "user" > $pwd/.build/userkind
|
||||
else
|
||||
echo "Use U or D keys to select kind of user/build process"
|
||||
echo "Use U or D keys (and press Enter) to select kind of user/build process"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
userkind=$(echo -n $(cat $pwd/.build/userkind))
|
||||
userkind=$(cat $pwd/.build/userkind)
|
||||
if [ "$userkind" == "developer" ] ; then
|
||||
echo "======================= BUILDING FOR DEVELOPER ======================="
|
||||
else
|
||||
|
|
@ -229,7 +221,7 @@ if [ ! -f "$pwd/.build/builds_dir" ] ; then
|
|||
echo "$builds_dir" > "$pwd/.build/builds_dir"
|
||||
fi
|
||||
# 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.
|
||||
builds_list="$(mktemp)"
|
||||
|
|
@ -265,7 +257,8 @@ else
|
|||
# New build
|
||||
if [[ "$build_n" == "n" || "$build_n" == "N" ]] ; then
|
||||
read -p "Select build type [RELEASE/debug]? "
|
||||
if [[ "${REPLY,,}" == "debug" ]] ; then
|
||||
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "${REPLY}" == "debug" ]] ; then
|
||||
build_type=Debug
|
||||
new_build_name=aseprite-debug
|
||||
else
|
||||
|
|
@ -348,10 +341,7 @@ else
|
|||
elif git --git-dir="$source_dir/.git" branch --contains "$remote/main" | grep -q "^\* $branch_name\$" ; then
|
||||
base_branch_name=main
|
||||
else
|
||||
echo ""
|
||||
echo "Error: Branch $branch_name looks like doesn't belong to main or beta"
|
||||
echo ""
|
||||
exit 1
|
||||
base_branch_name=$branch_name
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -366,15 +356,9 @@ else
|
|||
fi
|
||||
|
||||
# Required Skia for the base branch.
|
||||
if [ "$base_branch_name" == "beta" ] ; then
|
||||
skia_tag=m124-08a5439a6b
|
||||
file_skia_dir=beta_skia_dir
|
||||
possible_skia_dir_name=skia-m124
|
||||
else
|
||||
skia_tag=m102-861e4743af
|
||||
file_skia_dir=main_skia_dir
|
||||
possible_skia_dir_name=skia
|
||||
fi
|
||||
skia_tag=$(cat "$pwd/laf/misc/skia-tag.txt")
|
||||
possible_skia_dir_name=skia-$(echo $skia_tag | cut -d "-" -f 1)
|
||||
file_skia_dir="$base_branch_name"_skia_dir
|
||||
|
||||
# Check Skia dependency.
|
||||
if [ ! -f "$pwd/.build/$file_skia_dir" ] ; then
|
||||
|
|
@ -385,23 +369,33 @@ if [ ! -f "$pwd/.build/$file_skia_dir" ] ; then
|
|||
skia_dir="$HOME/deps/$possible_skia_dir_name"
|
||||
fi
|
||||
|
||||
# Set default location if not found
|
||||
if [ ! -d "$skia_dir" ] ; then
|
||||
echo ""
|
||||
echo "Skia directory wasn't found."
|
||||
echo ""
|
||||
|
||||
echo "Select Skia directory to create [$skia_dir]? "
|
||||
if [ ! $auto ] ; then
|
||||
read skia_dir_read
|
||||
if [ "$skia_dir_read" != "" ] ; then
|
||||
skia_dir="$skia_dir_read"
|
||||
fi
|
||||
# Use .deps directory to download Skia for users (which is a
|
||||
# simple setup). In case of developers we'd prefer the shared
|
||||
# directory by default.
|
||||
if [ "$userkind" == "user" ] ; then
|
||||
skia_dir="$pwd/.deps/$possible_skia_dir_name"
|
||||
fi
|
||||
|
||||
if [ ! -d "$skia_dir" ] ; then
|
||||
echo ""
|
||||
echo "Skia directory wasn't found."
|
||||
echo ""
|
||||
|
||||
echo "Select Skia directory to create [$skia_dir]? "
|
||||
if [ ! $auto ] ; then
|
||||
read skia_dir_read
|
||||
if [ "$skia_dir_read" != "" ] ; then
|
||||
skia_dir="$skia_dir_read"
|
||||
fi
|
||||
fi
|
||||
mkdir -p $skia_dir || exit 1
|
||||
fi
|
||||
mkdir -p $skia_dir || exit 1
|
||||
fi
|
||||
echo $skia_dir > "$pwd/.build/$file_skia_dir"
|
||||
fi
|
||||
skia_dir=$(echo -n $(cat $pwd/.build/$file_skia_dir))
|
||||
skia_dir=$(cat $pwd/.build/$file_skia_dir)
|
||||
if [ ! -d "$skia_dir" ] ; then
|
||||
mkdir "$skia_dir"
|
||||
fi
|
||||
|
|
@ -421,25 +415,20 @@ if [ ! -d "$skia_library_dir" ] ; then
|
|||
echo "Skia library wasn't found."
|
||||
echo ""
|
||||
if [ ! $auto ] ; then
|
||||
read -sN 1 -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
|
||||
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
|
||||
fi
|
||||
if [[ $auto || "$REPLY" == "" || "$REPLY" == "y" || "$REPLY" == "Y" ]] ; then
|
||||
if [[ $auto || "$REPLY" == "" || "$REPLY" == "y" || "$REPLY" == "yes" ]] ; then
|
||||
if [[ $is_win && "$build_type" == "Debug" ]] ; then
|
||||
skia_build=Debug
|
||||
else
|
||||
skia_build=Release
|
||||
fi
|
||||
|
||||
if [ $is_win ] ; then
|
||||
skia_file=Skia-Windows-$skia_build-$cpu.zip
|
||||
elif [ $is_macos ] ; then
|
||||
skia_file=Skia-macOS-$skia_build-$cpu.zip
|
||||
else
|
||||
skia_file=Skia-Linux-$skia_build-$cpu-libstdc++.zip
|
||||
fi
|
||||
skia_url=https://github.com/aseprite/skia/releases/download/$skia_tag/$skia_file
|
||||
skia_url=$(bash laf/misc/skia-url.sh $skia_build)
|
||||
skia_file=$(basename $skia_url)
|
||||
if [ ! -f "$skia_dir/$skia_file" ] ; then
|
||||
curl -L -o "$skia_dir/$skia_file" "$skia_url"
|
||||
curl --ssl-revoke-best-effort -L -o "$skia_dir/$skia_file" "$skia_url"
|
||||
fi
|
||||
if [ ! -d "$skia_library_dir" ] ; then
|
||||
unzip -n -d "$skia_dir" "$skia_dir/$skia_file"
|
||||
|
|
@ -468,7 +457,7 @@ if [ ! -f "$active_build_dir/ninja.build" ] ; then
|
|||
echo "This will take some minutes."
|
||||
echo ""
|
||||
if [ ! $auto ] ; then
|
||||
read -sN 1 -p "Press any key to continue. "
|
||||
read -p "Press Enter to continue."
|
||||
fi
|
||||
|
||||
if [ $is_macos ] ; then
|
||||
|
|
|
|||
|
|
@ -485,25 +485,25 @@
|
|||
<icon part="window_close_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_close_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_center_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_center_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_center_icon" color="button_normal_text" />
|
||||
<icon part="window_center_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_center_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_play_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_play_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_play_icon" color="button_normal_text" />
|
||||
<icon part="window_play_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_play_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_stop_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_stop_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_stop_icon" color="button_normal_text" />
|
||||
<icon part="window_stop_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_stop_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_help_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_help_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_help_icon" color="button_normal_text" />
|
||||
<icon part="window_help_icon" color="button_hot_text" state="mouse" />
|
||||
|
|
@ -556,7 +556,7 @@
|
|||
<style id="list_header_label" padding="2">
|
||||
<text color="text" align="left" x="2" />
|
||||
</style>
|
||||
<style id="link">
|
||||
<style id="link" padding="1">
|
||||
<text color="link_text" align="left" />
|
||||
<text color="link_hover" align="left" state="mouse" />
|
||||
</style>
|
||||
|
|
@ -709,10 +709,11 @@
|
|||
<style id="workspace_splitter">
|
||||
<background color="workspace" />
|
||||
</style>
|
||||
<style id="horizontal_separator" border-left="2" border-top="4" border-right="2" border-bottom="0">
|
||||
<style id="horizontal_separator" border="2">
|
||||
<background color="window_face" />
|
||||
<background-border part="separator_horz" align="middle" />
|
||||
<text color="separator_label" x="4" align="left middle" />
|
||||
<text color="disabled" x="4" align="left middle" state="disabled"/>
|
||||
</style>
|
||||
<style id="menu_separator" extends="horizontal_separator" />
|
||||
<style id="separator_in_view" extends="horizontal_separator">
|
||||
|
|
|
|||
|
|
@ -481,25 +481,25 @@
|
|||
<icon part="window_close_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_close_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_center_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_center_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_center_icon" color="button_normal_text" />
|
||||
<icon part="window_center_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_center_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_play_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_play_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_play_icon" color="button_normal_text" />
|
||||
<icon part="window_play_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_play_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_stop_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_stop_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_stop_icon" color="button_normal_text" />
|
||||
<icon part="window_stop_icon" color="button_hot_text" state="mouse" />
|
||||
<icon part="window_stop_icon" color="button_selected_text" state="selected" />
|
||||
</style>
|
||||
<style id="window_help_button" extends="window_button" margin-top="3" margin-right="2">
|
||||
<style id="window_help_button" extends="window_button" margin-top="3" margin-right="1">
|
||||
<newlayer />
|
||||
<icon part="window_help_icon" color="button_normal_text" />
|
||||
<icon part="window_help_icon" color="button_hot_text" state="mouse" />
|
||||
|
|
@ -549,7 +549,7 @@
|
|||
<style id="list_header_label" padding="2">
|
||||
<text color="text" align="left" x="2" />
|
||||
</style>
|
||||
<style id="link">
|
||||
<style id="link" padding="1">
|
||||
<text color="link_text" align="left" />
|
||||
<text color="link_hover" align="left" state="mouse" />
|
||||
</style>
|
||||
|
|
@ -702,10 +702,11 @@
|
|||
<style id="workspace_splitter">
|
||||
<background color="workspace" />
|
||||
</style>
|
||||
<style id="horizontal_separator" border-left="2" border-top="4" border-right="2" border-bottom="0">
|
||||
<style id="horizontal_separator" border="2">
|
||||
<background color="window_face" />
|
||||
<background-border part="separator_horz" align="middle" />
|
||||
<text color="separator_label" x="4" align="left middle" />
|
||||
<text color="disabled" x="4" align="left middle" state="disabled"/>
|
||||
</style>
|
||||
<style id="menu_separator" 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 |
|
|
@ -32,12 +32,14 @@
|
|||
|
||||
<font name="Aseprite"
|
||||
type="spritesheet"
|
||||
descent="2"
|
||||
file="aseprite_font.png">
|
||||
<fallback font="Unicode" size="8" />
|
||||
</font>
|
||||
|
||||
<font name="Aseprite Mini"
|
||||
type="spritesheet"
|
||||
descent="1"
|
||||
file="aseprite_mini.png">
|
||||
<fallback font="Unicode" size="6" />
|
||||
</font>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2014-2018 David Capello -->
|
||||
<preferences>
|
||||
|
||||
|
|
@ -550,6 +550,7 @@
|
|||
<option id="enabled" type="bool" default="false" />
|
||||
<option id="overlay_enabled" type="bool" default="false" />
|
||||
<option id="overlay_size" type="int" default="5" />
|
||||
<option id="scale_up_to_fit" type="bool" default="false" />
|
||||
</section>
|
||||
<section id="onionskin">
|
||||
<option id="active" type="bool" default="false" />
|
||||
|
|
|
|||
|
|
@ -785,6 +785,11 @@ load = Load External Font
|
|||
select_truetype_fonts = Select a Font File
|
||||
empty_fonts = No system fonts were found
|
||||
|
||||
[font_style]
|
||||
antialias = Antialias
|
||||
hinting = Hinting
|
||||
ligatures = Ligatures
|
||||
|
||||
[frame_combo]
|
||||
all_frames = All frames
|
||||
selected_frames = Selected frames
|
||||
|
|
@ -1329,7 +1334,7 @@ theme_mode = Theme Mode:
|
|||
screen_scaling = Screen Scaling:
|
||||
ui_scaling = UI Element Scaling:
|
||||
language = Language:
|
||||
download_translations = Download Translations
|
||||
font_warning = Customize font for this language
|
||||
gpu_acceleration = GPU acceleration [DEVMODE/INTERNAL TESTING ONLY]
|
||||
gpu_acceleration_tooltip = Check this option to enable hardware acceleration
|
||||
show_menu_bar = Show Aseprite menu bar
|
||||
|
|
@ -1666,6 +1671,11 @@ n_slices_removed = {} slice(s) removed
|
|||
x_removed = Layer "{}" removed
|
||||
layers_removed = Layers removed
|
||||
|
||||
[resource_listbox]
|
||||
loading = Loading
|
||||
pin = Pin this item
|
||||
unpin = Unpin this item
|
||||
|
||||
[save_file]
|
||||
title = Save File
|
||||
save_as = Save As
|
||||
|
|
@ -1682,7 +1692,8 @@ title = Save Selection (.msk file)
|
|||
[script_access]
|
||||
title = Security
|
||||
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:
|
||||
websocket_label = wants to open a WebSocket connection to this URL:
|
||||
clipboard_label = wants to access the system clipboard
|
||||
|
|
@ -1696,7 +1707,7 @@ allow_load_lib_access = &Allow Load External Library
|
|||
give_full_access = Give Script Full &Access
|
||||
stop_script = &Stop Script
|
||||
|
||||
[select_accelerator]
|
||||
[select_shortcut]
|
||||
title = Keyboard Shortcut
|
||||
key = Key:
|
||||
clear = Clear
|
||||
|
|
@ -1833,6 +1844,7 @@ first_frame = First Frame:
|
|||
thumbnails = Thumbnails
|
||||
thumbnail_size = Thumbnail Size:
|
||||
overlay_size = Overlay Size:
|
||||
scale_up_to_fit = Scale up to fit
|
||||
onion_skin = Onion Skin:
|
||||
merge_frames = Merge Frames
|
||||
red_blue_tint = Red/Blue Tint
|
||||
|
|
@ -1845,6 +1857,7 @@ behind_sprite = Behind sprite
|
|||
behind_sprite_toolip = Only for transparent layers.\nBackground is not included in this onion skin mode.
|
||||
in_front = In front of sprite
|
||||
in_front_toolip = For all kind of layers (background and transparent)
|
||||
set_as_defaults = Set as Defaults
|
||||
|
||||
[tools]
|
||||
rectangular_marquee = Rectangular Marquee Tool
|
||||
|
|
|
|||
|
|
@ -1,29 +1,14 @@
|
|||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2018 David Capello -->
|
||||
<gui i18nwarnings="false">
|
||||
<window id="about" text="About Aseprite">
|
||||
<vbox>
|
||||
<label text="" id="title" />
|
||||
<label text="Animated sprite editor && pixel art tool" />
|
||||
<hbox homogeneous="true">
|
||||
<hbox>
|
||||
<vbox expansive="true">
|
||||
<separator text="Developer Team" horizontal="true" />
|
||||
<link text="David Capello" url="https://twitter.com/davidcapello" />
|
||||
<link text="Gaspar Capello" url="https://twitter.com/Gasparoken" />
|
||||
<link text="Martín Capello" url="https://twitter.com/martincapell0" />
|
||||
<vbox minheight="8" />
|
||||
</vbox>
|
||||
<separator vertical="true" />
|
||||
</hbox>
|
||||
<vbox>
|
||||
<separator text="Credits" horizontal="true" cell_hspan="2" />
|
||||
<link text="Contributors" url="" id="credits" />
|
||||
<link text="Translators" url="" id="i18n_credits" />
|
||||
<link text="Open Source Projects" url="" id="licenses" />
|
||||
</vbox>
|
||||
</hbox>
|
||||
<link text="Authors && Credits" url="" id="credits" />
|
||||
<link text="Translators" url="" id="i18n_credits" />
|
||||
<link text="Open Source Projects" url="" id="licenses" />
|
||||
<separator horizontal="true" />
|
||||
<hbox>
|
||||
<label text="Copyright (C) 2001-2025" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2025 by Igara Studio S.A. -->
|
||||
<gui>
|
||||
<vbox id="font_style">
|
||||
<check id="antialias" text="@.antialias" />
|
||||
<check id="hinting" text="@.hinting" />
|
||||
<separator horizontal="true" />
|
||||
<check id="ligatures" text="@.ligatures" />
|
||||
</vbox>
|
||||
</gui>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2001-2018 David Capello -->
|
||||
|
|
@ -34,7 +35,7 @@
|
|||
<!-- General -->
|
||||
<vbox id="section_general">
|
||||
<separator text="@.section_general" horizontal="true" />
|
||||
<grid columns="3">
|
||||
<grid columns="2">
|
||||
<label text="@.ui_windows" />
|
||||
<hbox>
|
||||
<buttonset columns="2" id="ui_windows">
|
||||
|
|
@ -45,7 +46,6 @@
|
|||
<label text="@.theme_mode" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
<boxfiller />
|
||||
|
||||
<label text="@.screen_scaling" />
|
||||
<combobox id="screen_scale">
|
||||
|
|
@ -54,7 +54,6 @@
|
|||
<listitem text="300%" value="3" />
|
||||
<listitem text="400%" value="4" />
|
||||
</combobox>
|
||||
<boxfiller />
|
||||
|
||||
<label text="@.ui_scaling" />
|
||||
<combobox id="ui_scale">
|
||||
|
|
@ -63,12 +62,14 @@
|
|||
<listitem text="300%" value="3" />
|
||||
<listitem text="400%" value="4" />
|
||||
</combobox>
|
||||
<boxfiller />
|
||||
|
||||
<label text="@.language" />
|
||||
<link text="@.language" url="https://www.aseprite.org/languages/" />
|
||||
<combobox id="language" />
|
||||
<link text="@.download_translations" url="https://www.aseprite.org/languages/" />
|
||||
|
||||
<boxfiller id="font_warning_filler" visible="false" />
|
||||
<link text="@.font_warning" id="font_warning" visible="false" />
|
||||
</grid>
|
||||
<separator horizontal="true" />
|
||||
<check id="gpu_acceleration"
|
||||
text="@.gpu_acceleration"
|
||||
tooltip="@.gpu_acceleration_tooltip" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2025 by Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2001-2016 by David Capello -->
|
||||
<gui>
|
||||
<window id="select_accelerator" text="@.title">
|
||||
<window id="select_shortcut" text="@.title">
|
||||
<vbox expansive="true">
|
||||
<grid columns="3">
|
||||
<label text="@.key" />
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2025 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2014-2018 by David Capello -->
|
||||
<gui>
|
||||
<vbox id="timeline_conf">
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
|
||||
<check id="thumb_overlay_enabled" text="@.overlay_size"/>
|
||||
<slider min="2" max="10" id="thumb_overlay_size" cell_align="horizontal" width="128" />
|
||||
<check id="thumb_scale_up_to_fit" text="@.scale_up_to_fit" cell_hspan="2" />
|
||||
</grid>
|
||||
</vbox>
|
||||
</hbox>
|
||||
|
|
@ -55,5 +57,11 @@
|
|||
<radio group="2" text="@.in_front" id="infront" tooltip="@.in_front_toolip" />
|
||||
</hbox>
|
||||
</grid>
|
||||
|
||||
<separator horizontal="true" />
|
||||
<hbox>
|
||||
<boxfiller />
|
||||
<button id="defaults" text="@.set_as_defaults" />
|
||||
</hbox>
|
||||
</vbox>
|
||||
</gui>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,34 @@
|
|||
Aseprite uses the following open source projects:
|
||||
|
||||
* [Allegro 4](http://liballeg.org/)
|
||||
* [Bresenham algorithm implementations by Alois Zingl](http://members.chello.at/easyfilter/bresenham.html)
|
||||
* [cityhash](https://github.com/google/cityhash)
|
||||
* [cmark](https://github.com/jgm/cmark)
|
||||
* [curl](http://curl.haxx.se/)
|
||||
* [fmt](https://github.com/fmtlib/fmt)
|
||||
* [FreeType](http://www.freetype.org/)
|
||||
* [giflib](http://sourceforge.net/projects/giflib/)
|
||||
* [Google Test](https://github.com/google/googletest)
|
||||
* [harfbuzz](http://harfbuzz.org)
|
||||
* [IXWebSocket](https://github.com/machinezone/IXWebSocket)
|
||||
* [json11](https://github.com/dropbox/json11/)
|
||||
* [libarchive](http://www.libarchive.org/)
|
||||
* [libjpeg-turbo](https://libjpeg-turbo.org/)
|
||||
* [libpng](http://www.libpng.org/pub/png/)
|
||||
* [libwebp](https://developers.google.com/speed/webp/)
|
||||
* [Lua](https://www.lua.org/)
|
||||
* [pixman](http://www.pixman.org/)
|
||||
* [qoi](https://github.com/phoboslab/qoi)
|
||||
* [Sentry](https://sentry.io)
|
||||
* [skia](https://skia.org)
|
||||
* [simpleini](https://github.com/aseprite/simpleini/)
|
||||
* [TinyEXIF](https://github.com/cdcseacave/TinyEXIF)
|
||||
* [tinyexpr](https://github.com/codeplea/tinyexpr)
|
||||
* [tinyxml2](https://github.com/leethomason/tinyxml2)
|
||||
* [ucdn](https://github.com/grigorig/ucdn)
|
||||
* [Wintab API](http://www.wacomeng.com/windows/docs/WintabBackground.htm)
|
||||
* [zlib](http://www.zlib.net/)
|
||||
|
||||
# [Allegro 4](http://liballeg.org/)
|
||||
|
||||
```
|
||||
|
|
|
|||
2
laf
2
laf
|
|
@ -1 +1 @@
|
|||
Subproject commit 65829107c838817987f3cf6374cc68c583e5d538
|
||||
Subproject commit 7d30a582e5c8655fab368c4d61f3125855a7b30d
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Aseprite
|
||||
# Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
# Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
# Copyright (C) 2001-2018 David Capello
|
||||
|
||||
######################################################################
|
||||
|
|
@ -152,6 +152,12 @@ add_custom_command(
|
|||
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../README.md)
|
||||
list(APPEND out_data_files ${DATA_OUTPUT_DIR}/README.md)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${DATA_OUTPUT_DIR}/AUTHORS.md
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../AUTHORS.md ${DATA_OUTPUT_DIR}/AUTHORS.md
|
||||
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../AUTHORS.md)
|
||||
list(APPEND out_data_files ${DATA_OUTPUT_DIR}/AUTHORS.md)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${DATA_OUTPUT_DIR}/EULA.txt
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../EULA.txt ${DATA_OUTPUT_DIR}/EULA.txt
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Aseprite
|
||||
# Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
# Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
# Copyright (C) 2001-2018 David Capello
|
||||
|
||||
# Generate a ui::Widget for each widget in a XML file
|
||||
|
|
@ -98,17 +98,17 @@ add_library(app-lib ${generated_files})
|
|||
# These specific-platform files should be in an external library
|
||||
# (e.g. "base" or "os").
|
||||
if(WIN32)
|
||||
target_sources(app-lib PRIVATE font_path_win.cpp)
|
||||
target_sources(app-lib PRIVATE fonts/font_path_win.cpp)
|
||||
elseif(APPLE)
|
||||
target_sources(app-lib PRIVATE font_path_osx.mm)
|
||||
target_sources(app-lib PRIVATE fonts/font_path_osx.mm)
|
||||
else()
|
||||
target_sources(app-lib PRIVATE font_path_unix.cpp)
|
||||
target_sources(app-lib PRIVATE fonts/font_path_unix.cpp)
|
||||
endif()
|
||||
|
||||
# This defines a specific webp decoding utility function for using
|
||||
# in Windows when dragging and dropping images that are stored as
|
||||
# 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)
|
||||
endif()
|
||||
|
||||
|
|
@ -290,7 +290,6 @@ target_sources(app-lib PRIVATE
|
|||
cmd/copy_region.cpp
|
||||
cmd/crop_cel.cpp
|
||||
cmd/deselect_mask.cpp
|
||||
cmd/drop_on_timeline.cpp
|
||||
cmd/flatten_layers.cpp
|
||||
cmd/flip_image.cpp
|
||||
cmd/flip_mask.cpp
|
||||
|
|
@ -526,6 +525,7 @@ target_sources(app-lib PRIVATE
|
|||
context_flags.cpp
|
||||
doc.cpp
|
||||
doc_api.cpp
|
||||
doc_api_dnd_helper.cpp
|
||||
doc_diff.cpp
|
||||
doc_exporter.cpp
|
||||
doc_range_ops.cpp
|
||||
|
|
@ -544,8 +544,10 @@ target_sources(app-lib PRIVATE
|
|||
file_system.cpp
|
||||
filename_formatter.cpp
|
||||
flatten.cpp
|
||||
font_info.cpp
|
||||
font_path.cpp
|
||||
fonts/font_data.cpp
|
||||
fonts/font_info.cpp
|
||||
fonts/font_path.cpp
|
||||
fonts/fonts.cpp
|
||||
gui_xml.cpp
|
||||
i18n/strings.cpp
|
||||
i18n/xml_translator.cpp
|
||||
|
|
@ -667,9 +669,8 @@ target_sources(app-lib PRIVATE
|
|||
ui/rgbmap_algorithm_selector.cpp
|
||||
ui/sampling_selector.cpp
|
||||
ui/search_entry.cpp
|
||||
ui/select_accelerator.cpp
|
||||
ui/select_shortcut.cpp
|
||||
ui/selection_mode_field.cpp
|
||||
ui/skin/font_data.cpp
|
||||
ui/skin/skin_part.cpp
|
||||
ui/skin/skin_property.cpp
|
||||
ui/skin/skin_slider_property.cpp
|
||||
|
|
@ -683,6 +684,7 @@ target_sources(app-lib PRIVATE
|
|||
ui/tile_button.cpp
|
||||
ui/tileset_selector.cpp
|
||||
ui/timeline/ani_controls.cpp
|
||||
ui/timeline/doc_providers.cpp
|
||||
ui/timeline/timeline.cpp
|
||||
ui/toolbar.cpp
|
||||
ui/user_data_view.cpp
|
||||
|
|
@ -705,6 +707,7 @@ target_sources(app-lib PRIVATE
|
|||
util/layer_utils.cpp
|
||||
util/msk_file.cpp
|
||||
util/new_image_from_mask.cpp
|
||||
util/open_file_job.cpp
|
||||
util/pal_ops.cpp
|
||||
util/pic_file.cpp
|
||||
util/pixel_ratio.cpp
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
#include "os/x11/system.h"
|
||||
#endif
|
||||
|
||||
#if ENABLE_WEBP && LAF_WINDOWS
|
||||
#if ENABLE_WEBP && LAF_WINDOWS && LAF_SKIA
|
||||
#include "app/util/decode_webp.h"
|
||||
#endif
|
||||
|
||||
|
|
@ -485,7 +485,7 @@ void App::run(const bool runGuiManager)
|
|||
// How to interpret one finger on Windows tablets.
|
||||
manager->display()->nativeWindow()->setInterpretOneFingerGestureAsMouseMovement(
|
||||
preferences().experimental.oneFingerAsMouseMovement());
|
||||
#if ENABLE_WEBP
|
||||
#if ENABLE_WEBP && LAF_SKIA
|
||||
// In Windows we use a custom webp decoder for drag & drop operations.
|
||||
os::set_decode_webp(util::decode_webp);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -113,11 +113,11 @@ bool can_call_global_shortcut(const AppMenuItem::Native* native)
|
|||
(focus == nullptr || focus->type() != ui::kEntryWidget ||
|
||||
!is_text_entry_shortcut(native->shortcut)) &&
|
||||
(native->keyContext == KeyContext::Any ||
|
||||
native->keyContext == KeyboardShortcuts::instance()->getCurrentKeyContext());
|
||||
native->keyContext == KeyboardShortcuts::getCurrentKeyContext());
|
||||
}
|
||||
|
||||
// TODO this should be on "she" library (or we should use
|
||||
// os::Shortcut instead of ui::Accelerators)
|
||||
// TODO this should be on laf-os library (or we should use
|
||||
// os::Shortcut instead of ui::Shortcuts)
|
||||
int from_scancode_to_unicode(KeyScancode scancode)
|
||||
{
|
||||
static int map[] = {
|
||||
|
|
@ -284,22 +284,21 @@ void destroy_menu_item(ui::Widget* item)
|
|||
|
||||
os::Shortcut get_os_shortcut_from_key(const Key* key)
|
||||
{
|
||||
if (key && !key->accels().empty()) {
|
||||
const ui::Accelerator& accel = key->accels().front();
|
||||
if (key && !key->shortcuts().empty()) {
|
||||
const ui::Shortcut& shortcut = key->shortcuts().front();
|
||||
|
||||
#if LAF_MACOS
|
||||
// Shortcuts with spacebar as modifier do not work well in macOS
|
||||
// (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();
|
||||
#endif
|
||||
|
||||
return os::Shortcut(
|
||||
(accel.unicodeChar() ? accel.unicodeChar() : from_scancode_to_unicode(accel.scancode())),
|
||||
accel.modifiers());
|
||||
return os::Shortcut((shortcut.unicodeChar() ? shortcut.unicodeChar() :
|
||||
from_scancode_to_unicode(shortcut.scancode())),
|
||||
shortcut.modifiers());
|
||||
}
|
||||
else
|
||||
return os::Shortcut();
|
||||
return {};
|
||||
}
|
||||
|
||||
AppMenus* AppMenus::s_instance = nullptr;
|
||||
|
|
@ -719,7 +718,6 @@ Widget* AppMenus::convertXmlelemToMenuitem(XMLElement* elem, Menu* menu)
|
|||
{
|
||||
const char* id = elem->Attribute("id");
|
||||
const char* group = elem->Attribute("group");
|
||||
const char* standard = elem->Attribute("standard");
|
||||
|
||||
// is it a <separator>?
|
||||
if (strcmp(elem->Value(), "separator") == 0) {
|
||||
|
|
@ -781,6 +779,7 @@ Widget* AppMenus::convertXmlelemToMenuitem(XMLElement* elem, Menu* menu)
|
|||
}
|
||||
|
||||
#if LAF_MACOS
|
||||
const char* standard = elem->Attribute("standard");
|
||||
if (standard && strcmp(standard, "edit") == 0)
|
||||
menuitem->setAsStandardEditMenu();
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -20,7 +21,20 @@ namespace app { namespace cmd {
|
|||
|
||||
using namespace doc;
|
||||
|
||||
ClearRect::ClearRect(Cel* cel, const gfx::Rect& bounds, color_t color)
|
||||
{
|
||||
ASSERT(cel);
|
||||
initialize(cel, bounds, color);
|
||||
}
|
||||
|
||||
ClearRect::ClearRect(Cel* cel, const gfx::Rect& bounds)
|
||||
{
|
||||
ASSERT(cel);
|
||||
Doc* doc = static_cast<Doc*>(cel->document());
|
||||
initialize(cel, bounds, doc->bgColor(cel->layer()));
|
||||
}
|
||||
|
||||
void ClearRect::initialize(Cel* cel, const gfx::Rect& bounds, color_t color)
|
||||
{
|
||||
ASSERT(cel);
|
||||
|
||||
|
|
@ -37,9 +51,7 @@ ClearRect::ClearRect(Cel* cel, const gfx::Rect& bounds)
|
|||
return;
|
||||
|
||||
m_dstImage.reset(new WithImage(image));
|
||||
|
||||
Doc* doc = static_cast<Doc*>(cel->document());
|
||||
m_bgcolor = doc->bgColor(cel->layer());
|
||||
m_bgcolor = color;
|
||||
|
||||
m_copy.reset(crop_image(image, bounds2.x, bounds2.y, bounds2.w, bounds2.h, m_bgcolor));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -26,6 +27,7 @@ using namespace doc;
|
|||
class ClearRect : public Cmd {
|
||||
public:
|
||||
ClearRect(Cel* cel, const gfx::Rect& bounds);
|
||||
ClearRect(Cel* cel, const gfx::Rect& bounds, color_t color);
|
||||
|
||||
protected:
|
||||
void onExecute() override;
|
||||
|
|
@ -37,6 +39,7 @@ protected:
|
|||
}
|
||||
|
||||
private:
|
||||
void initialize(Cel* cel, const gfx::Rect& bounds, color_t color);
|
||||
void clear();
|
||||
void restore();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2020-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -44,7 +44,7 @@ void AboutCommand::onExecute(Context* context)
|
|||
});
|
||||
window.credits()->Click.connect([&window] {
|
||||
window.closeWindow(nullptr);
|
||||
App::instance()->mainWindow()->showBrowser("README.md", "Authors");
|
||||
App::instance()->mainWindow()->showBrowser("AUTHORS.md", "Authors");
|
||||
});
|
||||
window.i18nCredits()->Click.connect([&window] {
|
||||
window.closeWindow(nullptr);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -54,11 +55,11 @@ void AdvancedModeCommand::onExecute(Context* context)
|
|||
|
||||
if (oldMode == MainWindow::NormalMode && pref.advancedMode.showAlert()) {
|
||||
KeyPtr key = KeyboardShortcuts::instance()->command(this->id().c_str());
|
||||
if (!key->accels().empty()) {
|
||||
if (!key->shortcuts().empty()) {
|
||||
app::gen::AdvancedMode window;
|
||||
|
||||
window.warningLabel()->setTextf("You can go back pressing \"%s\" key.",
|
||||
key->accels().front().toString().c_str());
|
||||
key->shortcuts().front().toString().c_str());
|
||||
|
||||
window.openWindowInForeground();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
// Copyright (C) 2001-2025 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
|
@ -51,7 +51,7 @@ void DuplicateLayerCommand::onExecute(Context* context)
|
|||
Tx tx(writer, "Layer Duplication");
|
||||
LayerImage* sourceLayer = static_cast<LayerImage*>(writer.layer());
|
||||
DocApi api = document->getApi(tx);
|
||||
api.duplicateLayerAfter(sourceLayer, sourceLayer->parent(), sourceLayer);
|
||||
api.duplicateLayerAfter(sourceLayer, sourceLayer->parent(), sourceLayer, " Copy");
|
||||
tx.commit();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
#include "app/ui/app_menuitem.h"
|
||||
#include "app/ui/keyboard_shortcuts.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/skin/skin_theme.h"
|
||||
#include "base/fs.h"
|
||||
|
|
@ -151,7 +151,7 @@ public:
|
|||
, m_keyOrig(key ? new Key(*key) : nullptr)
|
||||
, m_menuitem(menuitem)
|
||||
, m_level(level)
|
||||
, m_hotAccel(-1)
|
||||
, m_hotShortcut(-1)
|
||||
, m_lockButtons(false)
|
||||
, m_headerItem(headerItem)
|
||||
{
|
||||
|
|
@ -204,45 +204,45 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
void onChangeAccel(int index)
|
||||
void onChangeShortcut(int index)
|
||||
{
|
||||
LockButtons lock(this);
|
||||
Accelerator origAccel = m_key->accels()[index];
|
||||
SelectAccelerator window(origAccel, m_key->keycontext(), m_keys);
|
||||
Shortcut origShortcut = m_key->shortcuts()[index];
|
||||
SelectShortcut window(origShortcut, m_key->keycontext(), m_keys);
|
||||
window.openWindowInForeground();
|
||||
|
||||
if (window.isModified()) {
|
||||
m_key->disableAccel(origAccel, KeySource::UserDefined);
|
||||
if (!window.accel().isEmpty())
|
||||
m_key->add(window.accel(), KeySource::UserDefined, m_keys);
|
||||
m_key->disableShortcut(origShortcut, KeySource::UserDefined);
|
||||
if (!window.shortcut().isEmpty())
|
||||
m_key->add(window.shortcut(), KeySource::UserDefined, m_keys);
|
||||
}
|
||||
|
||||
this->window()->layout();
|
||||
}
|
||||
|
||||
void onDeleteAccel(int index)
|
||||
void onDeleteShortcut(int index)
|
||||
{
|
||||
LockButtons lock(this);
|
||||
// We need to create a copy of the accelerator because
|
||||
// Key::disableAccel() will modify the accels() collection itself.
|
||||
ui::Accelerator accel = m_key->accels()[index];
|
||||
// We need to create a copy of the shortcut because
|
||||
// Key::disableShortcut() will modify the shortcuts() collection itself.
|
||||
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;
|
||||
|
||||
m_key->disableAccel(accel, KeySource::UserDefined);
|
||||
m_key->disableShortcut(shortcut, KeySource::UserDefined);
|
||||
window()->layout();
|
||||
}
|
||||
|
||||
void onAddAccel()
|
||||
void onAddShortcut()
|
||||
{
|
||||
LockButtons lock(this);
|
||||
ui::Accelerator accel;
|
||||
SelectAccelerator window(accel, m_key ? m_key->keycontext() : KeyContext::Any, m_keys);
|
||||
ui::Shortcut shortcut;
|
||||
SelectShortcut window(shortcut, m_key ? m_key->keycontext() : KeyContext::Any, m_keys);
|
||||
window.openWindowInForeground();
|
||||
|
||||
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())) {
|
||||
if (!m_key) {
|
||||
ASSERT(m_menuitem);
|
||||
|
|
@ -256,7 +256,7 @@ private:
|
|||
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();
|
||||
|
|
@ -273,8 +273,8 @@ private:
|
|||
size.w = std::max(size.w, w);
|
||||
}
|
||||
|
||||
if (m_key && !m_key->accels().empty()) {
|
||||
size_t combos = m_key->accels().size();
|
||||
if (m_key && !m_key->shortcuts().empty()) {
|
||||
size_t combos = m_key->shortcuts().size();
|
||||
if (combos > 1)
|
||||
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) {
|
||||
g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()),
|
||||
fg,
|
||||
|
|
@ -324,13 +324,14 @@ private:
|
|||
}
|
||||
|
||||
const int dh = th + 4 * guiscale();
|
||||
IntersectClip clip(g,
|
||||
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->accels().size()));
|
||||
IntersectClip clip(
|
||||
g,
|
||||
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->shortcuts().size()));
|
||||
if (clip) {
|
||||
int i = 0;
|
||||
for (const Accelerator& accel : m_key->accels()) {
|
||||
if (i != m_hotAccel || !m_changeButton) {
|
||||
g->drawText(getAccelText(accel), fg, bg, gfx::Point(keyXPos, y));
|
||||
for (const Shortcut& shortcut : m_key->shortcuts()) {
|
||||
if (i != m_hotShortcut || !m_changeButton) {
|
||||
g->drawText(getShortcutText(shortcut), fg, bg, gfx::Point(keyXPos, y));
|
||||
}
|
||||
y += dh;
|
||||
++i;
|
||||
|
|
@ -361,40 +362,41 @@ private:
|
|||
gfx::Rect bounds = this->bounds();
|
||||
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 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) {
|
||||
int w = font()->textLength(
|
||||
(accels && i < (int)accels->size() ? getAccelText((*accels)[i]) : std::string()));
|
||||
int w = font()->textLength((shortcuts && i < (int)shortcuts->size() ?
|
||||
getShortcutText((*shortcuts)[i]) :
|
||||
std::string()));
|
||||
gfx::Rect itemBounds(bounds.x + m_headerItem->keyXPos(), y, w, dh);
|
||||
itemBounds = itemBounds.enlarge(
|
||||
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) {
|
||||
if (m_hotAccel != i) {
|
||||
m_hotAccel = i;
|
||||
if (m_hotShortcut != i) {
|
||||
m_hotShortcut = i;
|
||||
|
||||
m_changeConn = obs::connection();
|
||||
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());
|
||||
addChild(m_changeButton.get());
|
||||
|
||||
m_deleteConn = obs::connection();
|
||||
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());
|
||||
addChild(m_deleteButton.get());
|
||||
|
||||
m_changeButton->setBgColor(gfx::ColorNone);
|
||||
m_changeButton->setBounds(itemBounds);
|
||||
m_changeButton->setText(getAccelText((*accels)[i]));
|
||||
m_changeButton->setText(getShortcutText((*shortcuts)[i]));
|
||||
|
||||
const char* label = "x";
|
||||
m_deleteButton->setBgColor(gfx::ColorNone);
|
||||
|
|
@ -411,7 +413,7 @@ private:
|
|||
if (i == 0 && !m_addButton && (!m_menuitem || m_menuitem->getCommand())) {
|
||||
m_addConn = obs::connection();
|
||||
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());
|
||||
addChild(m_addButton.get());
|
||||
|
||||
|
|
@ -452,17 +454,15 @@ private:
|
|||
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();
|
||||
}
|
||||
else {
|
||||
return accel.toString();
|
||||
}
|
||||
return shortcut.toString();
|
||||
}
|
||||
|
||||
KeyboardShortcuts& m_keys;
|
||||
|
|
@ -471,14 +471,14 @@ private:
|
|||
KeyPtr m_keyOrig;
|
||||
AppMenuItem* m_menuitem;
|
||||
int m_level;
|
||||
ui::Accelerators m_newAccels;
|
||||
ui::Shortcuts m_newShortcuts;
|
||||
std::shared_ptr<ui::Button> m_changeButton;
|
||||
std::shared_ptr<ui::Button> m_deleteButton;
|
||||
std::shared_ptr<ui::Button> m_addButton;
|
||||
obs::scoped_connection m_changeConn;
|
||||
obs::scoped_connection m_deleteConn;
|
||||
obs::scoped_connection m_addConn;
|
||||
int m_hotAccel;
|
||||
int m_hotShortcut;
|
||||
bool m_lockButtons;
|
||||
HeaderItem* m_headerItem;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -135,8 +135,16 @@ public:
|
|||
|
||||
remapWindow();
|
||||
centerWindow();
|
||||
|
||||
gfx::Rect originalBounds = bounds();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +167,11 @@ public:
|
|||
|
||||
if (countLayers() > 0) {
|
||||
m_userDataView.configureAndSet(m_layer->userData(), g_window->propertiesGrid());
|
||||
if (m_remapAfterConfigure) {
|
||||
remapWindow();
|
||||
centerWindow();
|
||||
m_remapAfterConfigure = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateFromLayer();
|
||||
|
|
@ -464,7 +477,7 @@ private:
|
|||
m_userDataView.setVisible(false, false);
|
||||
}
|
||||
|
||||
bool uuidVisible = m_document && m_document->sprite() && m_document->sprite()->uuidsForLayers();
|
||||
bool uuidVisible = m_document && m_document->sprite() && m_document->sprite()->useLayerUuids();
|
||||
uuidLabel()->setVisible(uuidVisible);
|
||||
uuid()->setVisible(uuidVisible);
|
||||
|
||||
|
|
@ -484,6 +497,7 @@ private:
|
|||
view::RealRange m_range;
|
||||
bool m_selfUpdate = false;
|
||||
UserDataView m_userDataView;
|
||||
bool m_remapAfterConfigure = false;
|
||||
};
|
||||
|
||||
LayerPropertiesCommand::LayerPropertiesCommand()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// 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("slot", base::convert_to<std::string>(slot).c_str());
|
||||
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
|
||||
if (key && !key->accels().empty()) {
|
||||
if (key && !key->shortcuts().empty()) {
|
||||
std::string tooltip;
|
||||
tooltip += Strings::new_brush_shortcut() + " ";
|
||||
tooltip += key->accels().front().toString();
|
||||
tooltip += key->shortcuts().front().toString();
|
||||
StatusBar::instance()->showTip(2000, tooltip);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -268,11 +268,15 @@ void NewLayerCommand::onExecute(Context* context)
|
|||
|
||||
switch (m_type) {
|
||||
case Type::Layer:
|
||||
layer = api.newLayer(parent, name);
|
||||
if (m_place == Place::BeforeActiveLayer)
|
||||
|
||||
if (m_place == Place::BeforeActiveLayer) {
|
||||
layer = api.newLayer(parent, name);
|
||||
api.restackLayerBefore(layer, parent, activeLayer);
|
||||
}
|
||||
else
|
||||
layer = api.newLayerAfter(parent, name, activeLayer);
|
||||
break;
|
||||
case Type::Group: layer = api.newGroup(parent, name); break;
|
||||
case Type::Group: layer = api.newGroupAfter(parent, name, activeLayer); break;
|
||||
case Type::ReferenceLayer:
|
||||
layer = api.newLayer(parent, name);
|
||||
if (layer)
|
||||
|
|
@ -296,9 +300,7 @@ void NewLayerCommand::onExecute(Context* context)
|
|||
tsi = tilesetInfo.tsi;
|
||||
}
|
||||
|
||||
layer = new LayerTilemap(sprite, tsi);
|
||||
layer->setName(name);
|
||||
api.addLayer(parent, layer, parent->lastLayer());
|
||||
layer = api.newTilemapAfter(parent, name, tsi, activeLayer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -320,10 +322,6 @@ void NewLayerCommand::onExecute(Context* context)
|
|||
api.restackLayerBefore(layer, sprite->root(), first);
|
||||
}
|
||||
}
|
||||
// Move the layer above the active one.
|
||||
else if (activeLayer && m_place == Place::AfterActiveLayer) {
|
||||
api.restackLayerAfter(layer, activeLayer->parent(), activeLayer);
|
||||
}
|
||||
|
||||
// Put all selected layers inside the group
|
||||
if (m_type == Type::Group && site.inTimeline()) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
#include "app/extensions.h"
|
||||
#include "app/file/file.h"
|
||||
#include "app/file_selector.h"
|
||||
#include "app/fonts/font_data.h"
|
||||
#include "app/fonts/fonts.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/ini_file.h"
|
||||
#include "app/launcher.h"
|
||||
|
|
@ -34,7 +36,6 @@
|
|||
#include "app/ui/rgbmap_algorithm_selector.h"
|
||||
#include "app/ui/sampling_selector.h"
|
||||
#include "app/ui/separator_in_view.h"
|
||||
#include "app/ui/skin/font_data.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "app/util/render_text.h"
|
||||
#include "base/convert_to.h"
|
||||
|
|
@ -163,7 +164,11 @@ class OptionsWindow : public app::gen::Options {
|
|||
|
||||
class LangItem : public ListItem {
|
||||
public:
|
||||
LangItem(const LangInfo& langInfo) : ListItem(langInfo.displayName), m_langInfo(langInfo) {}
|
||||
LangItem(const LangInfo& langInfo)
|
||||
: ListItem(fmt::format("{} ({})", langInfo.displayName, langInfo.id))
|
||||
, m_langInfo(langInfo)
|
||||
{
|
||||
}
|
||||
const std::string& langId() const { return m_langInfo.id; }
|
||||
|
||||
private:
|
||||
|
|
@ -274,6 +279,17 @@ public:
|
|||
fillThemeFonts();
|
||||
updateFontPreviews();
|
||||
|
||||
// Language change
|
||||
language()->Change.connect([this] { onLanguageChange(); });
|
||||
fontWarning()->Click.connect([this] {
|
||||
for (auto item : sectionListbox()->children()) {
|
||||
if (static_cast<ListItem*>(item)->getValue() == kSectionThemeId) {
|
||||
sectionListbox()->selectChild(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Recent files
|
||||
clearRecentFiles()->Click.connect([this] { onClearRecentFiles(); });
|
||||
|
||||
|
|
@ -447,11 +463,11 @@ public:
|
|||
// Theme Custom Font
|
||||
customThemeFont()->Click.connect([this] {
|
||||
auto* theme = skin::SkinTheme::get(this);
|
||||
onSwitchCustomFontCheckBox(customThemeFont(), themeFont(), theme->getOriginalDefaultFont());
|
||||
onSwitchCustomFontCheckBox(customThemeFont(), themeFont(), theme->getDefaultFontInfo());
|
||||
});
|
||||
customMiniFont()->Click.connect([this] {
|
||||
auto* theme = skin::SkinTheme::get(this);
|
||||
onSwitchCustomFontCheckBox(customMiniFont(), themeMiniFont(), theme->getOriginalMiniFont());
|
||||
onSwitchCustomFontCheckBox(customMiniFont(), themeMiniFont(), theme->getMiniFontInfo());
|
||||
});
|
||||
themeFont()->FontChange.connect([this] { updateFontPreviews(); });
|
||||
themeMiniFont()->FontChange.connect([this] { updateFontPreviews(); });
|
||||
|
|
@ -680,7 +696,6 @@ public:
|
|||
|
||||
onChangeBgScope();
|
||||
onChangeGridScope();
|
||||
sectionListbox()->selectIndex(m_curSection);
|
||||
|
||||
// Aseprite format preferences
|
||||
celFormat()->setSelectedItemIndex(int(m_pref.asepriteFormat.celFormat()));
|
||||
|
|
@ -916,8 +931,10 @@ public:
|
|||
// Change theme font
|
||||
bool reset_theme = false;
|
||||
{
|
||||
const FontInfo fontInfo = themeFont()->info();
|
||||
const FontInfo miniFontInfo = themeMiniFont()->info();
|
||||
const FontInfo fontInfo = (customThemeFont()->isSelected() ? themeFont()->info() :
|
||||
FontInfo());
|
||||
const FontInfo miniFontInfo = (customMiniFont()->isSelected() ? themeMiniFont()->info() :
|
||||
FontInfo());
|
||||
|
||||
auto fontStr = base::convert_to<std::string>(fontInfo);
|
||||
auto miniFontStr = base::convert_to<std::string>(miniFontInfo);
|
||||
|
|
@ -1028,6 +1045,9 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
void onOpen(Event& evt) override { sectionListbox()->selectIndex(m_curSection); }
|
||||
|
||||
private:
|
||||
void onInitTheme(InitThemeEvent& ev) override
|
||||
{
|
||||
|
|
@ -1091,33 +1111,17 @@ private:
|
|||
themeMiniFont()->setInfo(miniInfo, FontEntry::From::Init);
|
||||
}
|
||||
|
||||
void onSwitchCustomFontCheckBox(CheckBox* fontCheckBox,
|
||||
FontEntry* fontEntry,
|
||||
const text::FontRef& themeFont)
|
||||
void onSwitchCustomFontCheckBox(CheckBox* fontCheckBox, FontEntry* fontEntry, const FontInfo& fi)
|
||||
{
|
||||
const bool state = fontCheckBox->isSelected();
|
||||
fontEntry->setEnabled(state);
|
||||
|
||||
FontInfo fi;
|
||||
auto* theme = skin::SkinTheme::get(this);
|
||||
text::FontMgrRef fontMgr = theme->fontMgr();
|
||||
for (auto kv : theme->getWellKnownFonts()) {
|
||||
if (kv.second->getFont(fontMgr, themeFont->height(), guiscale()) == themeFont) {
|
||||
fi = FontInfo(FontInfo::Type::Name,
|
||||
kv.first,
|
||||
themeFont->height(),
|
||||
text::FontStyle(),
|
||||
themeFont->antialias() ? FontInfo::Flags::Antialias : FontInfo::Flags::None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
fontEntry->setInfo(fi, FontEntry::From::Init);
|
||||
}
|
||||
|
||||
void updateFontPreviews()
|
||||
{
|
||||
m_font = get_font_from_info(themeFont()->info());
|
||||
m_miniFont = get_font_from_info(themeMiniFont()->info());
|
||||
m_font = Fonts::instance()->fontFromInfo(themeFont()->info());
|
||||
m_miniFont = Fonts::instance()->fontFromInfo(themeMiniFont()->info());
|
||||
if (!m_miniFont)
|
||||
m_miniFont = skin::SkinTheme::get(this)->getMiniFont();
|
||||
|
||||
|
|
@ -1358,6 +1362,19 @@ private:
|
|||
panel()->showChild(findChild(item->getValue().c_str()));
|
||||
}
|
||||
|
||||
void onLanguageChange()
|
||||
{
|
||||
auto* item = dynamic_cast<const LangItem*>(language()->getSelectedItem());
|
||||
if (!item)
|
||||
return;
|
||||
const std::string lang = item->langId();
|
||||
const bool state = (lang == "ar" || lang == "ja" || lang == "ko" || lang == "yue_Hant" ||
|
||||
lang == "zh_Hans" || lang == "zh_Hant");
|
||||
fontWarningFiller()->setVisible(state);
|
||||
fontWarning()->setVisible(state);
|
||||
layout();
|
||||
}
|
||||
|
||||
void onClearRecentFiles() { App::instance()->recentFiles()->clear(); }
|
||||
|
||||
void onColorManagement()
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ public:
|
|||
{
|
||||
userData()->Click.connect([this] { onToggleUserData(); });
|
||||
|
||||
useUuidForLayers()->setSelected(sprite->uuidsForLayers());
|
||||
useUuidForLayers()->setSelected(sprite->useLayerUuids());
|
||||
|
||||
m_userDataView.configureAndSet(m_sprite->userData(), propertiesGrid());
|
||||
|
||||
|
|
@ -399,7 +399,7 @@ void SpritePropertiesCommand::onExecute(Context* context)
|
|||
|
||||
const UserData newUserData = window.getUserData();
|
||||
|
||||
sprite->setUuidsForLayers(window.useUuidForLayers()->isSelected());
|
||||
sprite->useLayerUuids(window.useUuidForLayers()->isSelected());
|
||||
|
||||
if (index != sprite->transparentColor() || pixelRatio != sprite->pixelRatio() ||
|
||||
newUserData != sprite->userData()) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2020-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2015-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -28,10 +28,12 @@
|
|||
#include "app/ui/workspace.h"
|
||||
#include "base/mem_utils.h"
|
||||
#include "fmt/format.h"
|
||||
#include "text/font_metrics.h"
|
||||
#include "ui/init_theme_event.h"
|
||||
#include "ui/listitem.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/paint_event.h"
|
||||
#include "ui/scale.h"
|
||||
#include "ui/size_hint_event.h"
|
||||
#include "ui/view.h"
|
||||
#include "undo/undo_state.h"
|
||||
|
|
@ -300,9 +302,14 @@ public:
|
|||
style = theme->styles.undoSavedItem();
|
||||
}
|
||||
|
||||
text::FontMetrics metrics;
|
||||
font()->metrics(&metrics);
|
||||
const float lineHeight = metrics.descent - metrics.ascent;
|
||||
|
||||
ui::PaintWidgetPartInfo info;
|
||||
info.text = &itemText;
|
||||
info.styleFlags = (selected ? ui::Style::Layer::kSelected : 0);
|
||||
info.baseline = ui::guiscaled_center(itemBounds.y, itemBounds.h, lineHeight) - metrics.ascent;
|
||||
theme->paintWidgetPart(g, style, itemBounds, info);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ protected:
|
|||
for (const uint8_t* line : m_fileContent->lines) {
|
||||
ASSERT(line);
|
||||
tmp.assign((const char*)line);
|
||||
m_maxLineWidth = std::max(m_maxLineWidth, f->textLength(tmp));
|
||||
m_maxLineWidth = std::max<int>(m_maxLineWidth, std::ceil(f->textLength(tmp)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -128,7 +128,7 @@ private:
|
|||
case ui::kKeyDownMessage: {
|
||||
KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
||||
const KeyPtr key = keys->command(CommandId::SwitchColors());
|
||||
if (key && key->isPressed(msg, *keys)) {
|
||||
if (key && key->isPressed(msg)) {
|
||||
// Switch colors
|
||||
app::Color from = m_fromButton->getColor();
|
||||
app::Color to = m_toButton->getColor();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
#include "app/cmd/add_cel.h"
|
||||
#include "app/cmd/add_frame.h"
|
||||
#include "app/cmd/add_layer.h"
|
||||
#include "app/cmd/add_tileset.h"
|
||||
#include "app/cmd/clear_cel.h"
|
||||
#include "app/cmd/clear_image.h"
|
||||
#include "app/cmd/copy_cel.h"
|
||||
|
|
@ -52,6 +53,7 @@
|
|||
#include "doc/algorithm/flip_image.h"
|
||||
#include "doc/algorithm/shrink_bounds.h"
|
||||
#include "doc/cel.h"
|
||||
#include "doc/layer_tilemap.h"
|
||||
#include "doc/mask.h"
|
||||
#include "doc/palette.h"
|
||||
#include "doc/slice.h"
|
||||
|
|
@ -605,6 +607,18 @@ LayerImage* DocApi::newLayer(LayerGroup* parent, const std::string& name)
|
|||
return newLayer;
|
||||
}
|
||||
|
||||
LayerImage* DocApi::newLayerAfter(LayerGroup* parent, const std::string& name, Layer* afterThis)
|
||||
{
|
||||
LayerImage* newLayer = new LayerImage(parent->sprite());
|
||||
newLayer->setName(name);
|
||||
|
||||
if (!afterThis)
|
||||
afterThis = parent->lastLayer();
|
||||
|
||||
addLayer(parent, newLayer, afterThis);
|
||||
return newLayer;
|
||||
}
|
||||
|
||||
LayerGroup* DocApi::newGroup(LayerGroup* parent, const std::string& name)
|
||||
{
|
||||
LayerGroup* newLayerGroup = new LayerGroup(parent->sprite());
|
||||
|
|
@ -614,6 +628,33 @@ LayerGroup* DocApi::newGroup(LayerGroup* parent, const std::string& name)
|
|||
return newLayerGroup;
|
||||
}
|
||||
|
||||
LayerGroup* DocApi::newGroupAfter(LayerGroup* parent, const std::string& name, Layer* afterThis)
|
||||
{
|
||||
LayerGroup* newLayerGroup = new LayerGroup(parent->sprite());
|
||||
newLayerGroup->setName(name);
|
||||
|
||||
if (!afterThis)
|
||||
afterThis = parent->lastLayer();
|
||||
|
||||
addLayer(parent, newLayerGroup, afterThis);
|
||||
return newLayerGroup;
|
||||
}
|
||||
|
||||
LayerTilemap* DocApi::newTilemapAfter(LayerGroup* parent,
|
||||
const std::string& name,
|
||||
tileset_index tsi,
|
||||
Layer* afterThis)
|
||||
{
|
||||
LayerTilemap* newTilemap = new LayerTilemap(parent->sprite(), tsi);
|
||||
newTilemap->setName(name);
|
||||
|
||||
if (!afterThis)
|
||||
afterThis = parent->lastLayer();
|
||||
|
||||
addLayer(parent, newTilemap, afterThis);
|
||||
return newTilemap;
|
||||
}
|
||||
|
||||
void DocApi::addLayer(LayerGroup* parent, Layer* newLayer, Layer* afterThis)
|
||||
{
|
||||
m_transaction.execute(new cmd::AddLayer(parent, newLayer, afterThis));
|
||||
|
|
@ -652,23 +693,61 @@ void DocApi::restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeT
|
|||
restackLayerAfter(layer, parent, afterThis);
|
||||
}
|
||||
|
||||
Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer* afterLayer)
|
||||
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,
|
||||
const std::string& nameSuffix)
|
||||
{
|
||||
ASSERT(parent);
|
||||
Layer* newLayerPtr = copy_layer(sourceLayer);
|
||||
Layer* newLayerPtr = copyLayerWithSprite(sourceLayer, parent->sprite());
|
||||
|
||||
newLayerPtr->setName(newLayerPtr->name() + " Copy");
|
||||
newLayerPtr->setName(newLayerPtr->name() + nameSuffix);
|
||||
|
||||
addLayer(parent, newLayerPtr, afterLayer);
|
||||
|
||||
return newLayerPtr;
|
||||
}
|
||||
|
||||
Layer* DocApi::duplicateLayerBefore(Layer* sourceLayer, LayerGroup* parent, Layer* beforeLayer)
|
||||
Layer* DocApi::duplicateLayerBefore(Layer* sourceLayer,
|
||||
LayerGroup* parent,
|
||||
Layer* beforeLayer,
|
||||
const std::string& nameSuffix)
|
||||
{
|
||||
ASSERT(parent);
|
||||
Layer* afterThis = (beforeLayer ? beforeLayer->getPreviousBrowsable() : nullptr);
|
||||
Layer* newLayer = duplicateLayerAfter(sourceLayer, parent, afterThis);
|
||||
Layer* newLayer = duplicateLayerAfter(sourceLayer, parent, afterThis, nameSuffix);
|
||||
if (newLayer)
|
||||
restackLayerBefore(newLayer, parent, beforeLayer);
|
||||
return newLayer;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -9,12 +9,14 @@
|
|||
#define APP_DOC_API_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/doc_api_dnd_helper.h"
|
||||
#include "app/drop_frame_place.h"
|
||||
#include "app/tags_handling.h"
|
||||
#include "doc/algorithm/flip_type.h"
|
||||
#include "doc/color.h"
|
||||
#include "doc/frame.h"
|
||||
#include "doc/image_ref.h"
|
||||
#include "doc/tile.h"
|
||||
#include "gfx/rect.h"
|
||||
|
||||
#include <map>
|
||||
|
|
@ -26,6 +28,7 @@ class Image;
|
|||
class Layer;
|
||||
class LayerGroup;
|
||||
class LayerImage;
|
||||
class LayerTilemap;
|
||||
class Mask;
|
||||
class Palette;
|
||||
class Sprite;
|
||||
|
|
@ -36,6 +39,7 @@ class Doc;
|
|||
class Transaction;
|
||||
|
||||
using namespace doc;
|
||||
using namespace docapi;
|
||||
|
||||
// High-level API to modify a document adding undo information, i.e.
|
||||
// adding new "Cmd"s in the given transaction.
|
||||
|
|
@ -104,13 +108,25 @@ public:
|
|||
|
||||
// Layers API
|
||||
LayerImage* newLayer(LayerGroup* parent, const std::string& name);
|
||||
LayerImage* newLayerAfter(LayerGroup* parent, const std::string& name, Layer* afterThis);
|
||||
LayerGroup* newGroup(LayerGroup* parent, const std::string& name);
|
||||
LayerGroup* newGroupAfter(LayerGroup* parent, const std::string& name, Layer* afterThis);
|
||||
LayerTilemap* newTilemapAfter(LayerGroup* parent,
|
||||
const std::string& name,
|
||||
tileset_index tsi,
|
||||
Layer* afterThis);
|
||||
void addLayer(LayerGroup* parent, Layer* newLayer, Layer* afterThis);
|
||||
void removeLayer(Layer* layer);
|
||||
void restackLayerAfter(Layer* layer, LayerGroup* parent, Layer* afterThis);
|
||||
void restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeThis);
|
||||
Layer* duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer* afterLayer);
|
||||
Layer* duplicateLayerBefore(Layer* sourceLayer, LayerGroup* parent, Layer* beforeLayer);
|
||||
Layer* duplicateLayerAfter(Layer* sourceLayer,
|
||||
LayerGroup* parent,
|
||||
Layer* afterLayer,
|
||||
const std::string& nameSuffix = std::string());
|
||||
Layer* duplicateLayerBefore(Layer* sourceLayer,
|
||||
LayerGroup* parent,
|
||||
Layer* beforeLayer,
|
||||
const std::string& nameSuffix = std::string());
|
||||
|
||||
// Images API
|
||||
void replaceImage(Sprite* sprite, const ImageRef& oldImage, const ImageRef& newImage);
|
||||
|
|
@ -126,6 +142,14 @@ public:
|
|||
// Palette API
|
||||
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:
|
||||
void cropImageLayer(LayerImage* layer, const gfx::Rect& bounds, const bool trimOutside);
|
||||
bool cropCel(LayerImage* layer, Cel* cel, const gfx::Rect& bounds, const bool trimOutside);
|
||||
|
|
@ -137,6 +161,8 @@ private:
|
|||
const DropFramePlace dropFramePlace,
|
||||
const TagsHandling tagsHandling);
|
||||
|
||||
Layer* copyLayerWithSprite(doc::Layer* layer, doc::Sprite* sprite);
|
||||
|
||||
class HandleLinkedCels {
|
||||
public:
|
||||
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
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
#include "gfx/size.h"
|
||||
#include "render/dithering.h"
|
||||
#include "render/ordered_dither.h"
|
||||
#include "render/quantization.h"
|
||||
#include "render/render.h"
|
||||
#include "ver/info.h"
|
||||
|
||||
|
|
@ -300,6 +301,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() : nullptr,
|
||||
0,
|
||||
0,
|
||||
nullptr);
|
||||
m_image = convertedImg;
|
||||
}
|
||||
|
||||
private:
|
||||
Doc* m_document;
|
||||
Sprite* m_sprite;
|
||||
|
|
@ -679,6 +709,9 @@ Doc* DocExporter::exportSheet(Context* ctx, base::task_token& token)
|
|||
Sprite* texture = textureDocument->sprite();
|
||||
Image* textureImage = texture->root()->firstLayer()->cel(frame_t(0))->image();
|
||||
|
||||
for (auto& sample : samples)
|
||||
sample.setPixelFormat(texture->pixelFormat());
|
||||
|
||||
renderTexture(ctx, samples, textureImage, token);
|
||||
if (token.canceled())
|
||||
return nullptr;
|
||||
|
|
@ -1245,22 +1278,6 @@ void DocExporter::renderTexture(Context* ctx,
|
|||
++i;
|
||||
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.inTextureBounds().x + m_innerPadding,
|
||||
sample.inTextureBounds().y + m_innerPadding,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2020 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -405,7 +405,8 @@ static DocRange drop_range_op(Doc* doc,
|
|||
if (place == kDocRangeBefore) {
|
||||
Layer* beforeThis = (!dstLayers.empty() ? dstLayers.front() : nullptr);
|
||||
for (Layer* srcLayer : srcLayers) {
|
||||
Layer* copiedLayer = api.duplicateLayerBefore(srcLayer, parent, beforeThis);
|
||||
Layer* copiedLayer =
|
||||
api.duplicateLayerBefore(srcLayer, parent, beforeThis, " Copy");
|
||||
|
||||
resultRange.startRange(copiedLayer, -1, DocRange::kLayers);
|
||||
resultRange.endRange(copiedLayer, -1);
|
||||
|
|
@ -416,7 +417,7 @@ static DocRange drop_range_op(Doc* doc,
|
|||
|
||||
Layer* afterThis = (!dstLayers.empty() ? dstLayers.back() : nullptr);
|
||||
for (Layer* srcLayer : srcLayers) {
|
||||
Layer* copiedLayer = api.duplicateLayerAfter(srcLayer, parent, afterThis);
|
||||
Layer* copiedLayer = api.duplicateLayerAfter(srcLayer, parent, afterThis, " Copy");
|
||||
|
||||
resultRange.startRange(copiedLayer, -1, DocRange::kLayers);
|
||||
resultRange.endRange(copiedLayer, -1);
|
||||
|
|
|
|||
|
|
@ -544,7 +544,7 @@ static void ase_file_prepare_header(FILE* f,
|
|||
0);
|
||||
header->flags = (ASE_FILE_FLAG_LAYER_WITH_OPACITY |
|
||||
(composeGroups ? ASE_FILE_FLAG_COMPOSITE_GROUPS : 0) |
|
||||
(sprite->uuidsForLayers() ? ASE_FILE_FLAG_LAYER_WITH_UUID : 0));
|
||||
(sprite->useLayerUuids() ? ASE_FILE_FLAG_LAYER_WITH_UUID : 0));
|
||||
header->speed = sprite->frameDuration(firstFrame);
|
||||
header->next = 0;
|
||||
header->frit = 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// 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/fonts/font_data.h"
|
||||
|
||||
#include "text/font.h"
|
||||
#include "text/font_mgr.h"
|
||||
#include "text/sprite_sheet_font.h"
|
||||
#include "ui/manager.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
#define USE_CACHE 1
|
||||
|
||||
namespace app {
|
||||
|
||||
FontData::FontData(text::FontType type) : m_type(type)
|
||||
{
|
||||
}
|
||||
|
||||
FontData::FontData(const text::FontRef& nativeFont)
|
||||
: m_type(nativeFont->type())
|
||||
, m_antialias(nativeFont->antialias())
|
||||
, m_hinting(nativeFont->hinting())
|
||||
, m_nativeFont(nativeFont)
|
||||
{
|
||||
}
|
||||
|
||||
text::FontRef FontData::getFont(text::FontMgrRef& fontMgr, float size)
|
||||
{
|
||||
ASSERT(fontMgr);
|
||||
|
||||
if (size == 0.0f && m_size != 0.0f)
|
||||
size = m_size;
|
||||
|
||||
#if USE_CACHE
|
||||
// Use cached fonts
|
||||
const Cache::Key cacheKey{ size, m_antialias, m_hinting != text::FontHinting::None };
|
||||
auto it = m_cache.fonts.find(cacheKey);
|
||||
if (it != m_cache.fonts.end()) // Cache hit
|
||||
return it->second;
|
||||
#endif
|
||||
|
||||
text::FontRef font = nullptr;
|
||||
|
||||
switch (m_type) {
|
||||
case text::FontType::SpriteSheet:
|
||||
font = fontMgr->loadSpriteSheetFont(m_filename.c_str(), size);
|
||||
if (font && m_descent != 0.0f) {
|
||||
static_cast<text::SpriteSheetFont*>(font.get())->setDescent(m_descent);
|
||||
}
|
||||
break;
|
||||
|
||||
case text::FontType::FreeType: {
|
||||
font = fontMgr->loadTrueTypeFont(m_filename.c_str(), size);
|
||||
if (font) {
|
||||
font->setAntialias(m_antialias);
|
||||
font->setHinting(m_hinting);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case text::FontType::Native:
|
||||
if (size == m_nativeFont->size())
|
||||
font = m_nativeFont;
|
||||
else {
|
||||
text::TypefaceRef typeface = m_nativeFont->typeface();
|
||||
font = fontMgr->makeFont(typeface, size);
|
||||
font->setAntialias(m_antialias);
|
||||
font->setHinting(m_hinting);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
#if USE_CACHE
|
||||
// Cache this font
|
||||
m_cache.fonts[cacheKey] = font;
|
||||
#endif
|
||||
|
||||
// Load fallback
|
||||
if (m_fallback) {
|
||||
text::FontRef fallback = m_fallback->getFont(fontMgr, m_fallbackSize);
|
||||
if (font)
|
||||
font->setFallback(fallback.get());
|
||||
else
|
||||
return fallback; // Don't double-cache the fallback font
|
||||
}
|
||||
return font;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_FONTS_FONT_DATA_H_INCLUDED
|
||||
#define APP_FONTS_FONT_DATA_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "base/disable_copying.h"
|
||||
#include "text/font.h"
|
||||
#include "text/fwd.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace app {
|
||||
|
||||
// Represents a defined font in a <font> element from "data/fonts/fonts.xml" file
|
||||
// and theme fonts (<font> elements from "data/extensions/aseprite-theme/theme.xml").
|
||||
class FontData {
|
||||
public:
|
||||
FontData(text::FontType type);
|
||||
FontData(const text::FontRef& nativeFont);
|
||||
|
||||
text::FontType type() const { return m_type; }
|
||||
const std::string& name() const { return m_name; }
|
||||
const std::string& filename() const { return m_filename; }
|
||||
float defaultSize() const { return m_size; }
|
||||
bool antialias() const { return m_antialias; }
|
||||
text::FontHinting hinting() const { return m_hinting; }
|
||||
|
||||
void setName(const std::string& name) { m_name = name; }
|
||||
void setFilename(const std::string& filename) { m_filename = filename; }
|
||||
void setDefaultSize(const float size) { m_size = size; }
|
||||
void setAntialias(bool antialias) { m_antialias = antialias; }
|
||||
void setFallback(FontData* fallback, float fallbackSize)
|
||||
{
|
||||
m_fallback = fallback;
|
||||
m_fallbackSize = fallbackSize;
|
||||
}
|
||||
|
||||
// Descent font metrics for sprite sheet fonts
|
||||
void setDescent(float descent) { m_descent = descent; }
|
||||
|
||||
text::FontRef getFont(text::FontMgrRef& fontMgr, float size);
|
||||
|
||||
private:
|
||||
// Cache of loaded fonts so we avoid re-loading them.
|
||||
struct Cache {
|
||||
struct Key {
|
||||
float size;
|
||||
bool antialias : 1;
|
||||
bool hinting : 1;
|
||||
bool operator<(const Key& b) const
|
||||
{
|
||||
return size < b.size || antialias < b.antialias || hinting < b.hinting;
|
||||
}
|
||||
};
|
||||
std::map<Key, text::FontRef> fonts;
|
||||
};
|
||||
|
||||
text::FontType m_type;
|
||||
std::string m_name;
|
||||
std::string m_filename;
|
||||
float m_size = 0.0f;
|
||||
bool m_antialias = false;
|
||||
text::FontHinting m_hinting = text::FontHinting::Normal;
|
||||
Cache m_cache;
|
||||
FontData* m_fallback = nullptr;
|
||||
float m_fallbackSize = 0.0f;
|
||||
float m_descent = 0.0f;
|
||||
text::FontRef m_nativeFont;
|
||||
|
||||
DISABLE_COPYING(FontData);
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
||||
|
|
@ -8,7 +8,9 @@
|
|||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/font_info.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
|
||||
#include "app/fonts/font_data.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "base/fs.h"
|
||||
#include "base/split_string.h"
|
||||
|
|
@ -26,27 +28,46 @@ FontInfo::FontInfo(Type type,
|
|||
const std::string& name,
|
||||
const float size,
|
||||
const text::FontStyle style,
|
||||
const Flags flags)
|
||||
const Flags flags,
|
||||
const text::FontHinting hinting)
|
||||
: m_type(type)
|
||||
, m_name(name)
|
||||
, m_size(size)
|
||||
, m_style(style)
|
||||
, m_flags(flags)
|
||||
, m_hinting(hinting)
|
||||
{
|
||||
}
|
||||
|
||||
FontInfo::FontInfo(const FontInfo& other,
|
||||
const float size,
|
||||
const text::FontStyle style,
|
||||
const Flags flags)
|
||||
const Flags flags,
|
||||
text::FontHinting hinting)
|
||||
: m_type(other.type())
|
||||
, m_name(other.name())
|
||||
, m_size(size)
|
||||
, m_style(style)
|
||||
, m_flags(flags)
|
||||
, m_hinting(hinting)
|
||||
{
|
||||
}
|
||||
|
||||
FontInfo::FontInfo(const FontData* data, const float size)
|
||||
: m_type(Type::Unknown)
|
||||
, m_name(data->name())
|
||||
, m_size(size != 0.0f ? size : data->defaultSize())
|
||||
, m_flags(data->antialias() ? Flags::Antialias : Flags::None)
|
||||
, m_hinting(data->hinting())
|
||||
{
|
||||
switch (data->type()) {
|
||||
case text::FontType::Unknown: m_type = Type::Unknown; break;
|
||||
case text::FontType::SpriteSheet:
|
||||
case text::FontType::FreeType: m_type = Type::File; break;
|
||||
case text::FontType::Native: m_type = Type::System; break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string FontInfo::title() const
|
||||
{
|
||||
return m_type == FontInfo::Type::File ? base::get_file_name(m_name) : m_name;
|
||||
|
|
@ -129,6 +150,12 @@ std::string FontInfo::humanString() const
|
|||
result += " Antialias";
|
||||
if (ligatures())
|
||||
result += " Ligatures";
|
||||
switch (hinting()) {
|
||||
case text::FontHinting::None: result += " No Hinting"; break;
|
||||
case text::FontHinting::Slight: result += " Slight Hinting"; break;
|
||||
case text::FontHinting::Normal: break;
|
||||
case text::FontHinting::Full: result += " Full Hinting"; break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -149,6 +176,7 @@ app::FontInfo convert_to(const std::string& from)
|
|||
bool bold = false;
|
||||
bool italic = false;
|
||||
app::FontInfo::Flags flags = app::FontInfo::Flags::None;
|
||||
text::FontHinting hinting = text::FontHinting::Normal;
|
||||
|
||||
if (!parts.empty()) {
|
||||
if (parts[0].compare(0, 5, "file=") == 0) {
|
||||
|
|
@ -175,6 +203,17 @@ app::FontInfo convert_to(const std::string& from)
|
|||
else if (parts[i].compare(0, 5, "size=") == 0) {
|
||||
size = std::strtof(parts[i].substr(5).c_str(), nullptr);
|
||||
}
|
||||
else if (parts[i].compare(0, 8, "hinting=") == 0) {
|
||||
std::string hintingStr = parts[i].substr(8);
|
||||
if (hintingStr == "none")
|
||||
hinting = text::FontHinting::None;
|
||||
else if (hintingStr == "slight")
|
||||
hinting = text::FontHinting::Slight;
|
||||
else if (hintingStr == "normal")
|
||||
hinting = text::FontHinting::Normal;
|
||||
else if (hintingStr == "full")
|
||||
hinting = text::FontHinting::Full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +225,7 @@ app::FontInfo convert_to(const std::string& from)
|
|||
else if (italic)
|
||||
style = text::FontStyle::Italic();
|
||||
|
||||
return app::FontInfo(type, name, size, style, flags);
|
||||
return app::FontInfo(type, name, size, style, flags, hinting);
|
||||
}
|
||||
|
||||
template<>
|
||||
|
|
@ -212,6 +251,17 @@ std::string convert_to(const app::FontInfo& from)
|
|||
result += ",antialias";
|
||||
if (from.ligatures())
|
||||
result += ",ligatures";
|
||||
if (from.hinting() != text::FontHinting::Normal) {
|
||||
result += ",hinting=";
|
||||
switch (from.hinting()) {
|
||||
case text::FontHinting::None: result += "none"; break;
|
||||
case text::FontHinting::Slight: result += "slight"; break;
|
||||
case text::FontHinting::Normal:
|
||||
// Filtered out by above if
|
||||
break;
|
||||
case text::FontHinting::Full: result += "full"; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
// Aseprite
|
||||
// Copyright (c) 2024 Igara Studio S.A.
|
||||
// Copyright (c) 2024-2025 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_UI_FONT_INFO_H_INCLUDED
|
||||
#define APP_UI_FONT_INFO_H_INCLUDED
|
||||
#ifndef APP_FONTS_FONT_INFO_H_INCLUDED
|
||||
#define APP_FONTS_FONT_INFO_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "base/convert_to.h"
|
||||
#include "base/enum_flags.h"
|
||||
#include "text/font_hinting.h"
|
||||
#include "text/font_style.h"
|
||||
#include "text/font_type.h"
|
||||
#include "text/fwd.h"
|
||||
#include "text/typeface.h"
|
||||
|
||||
|
|
@ -19,7 +21,16 @@
|
|||
|
||||
namespace app {
|
||||
|
||||
// TODO should we merge this with skin::FontData?
|
||||
class FontData;
|
||||
|
||||
// Represents a font reference from any place:
|
||||
// - Name: a font referenced by name, a font that came from fonts.xml files (Fonts/FontData)
|
||||
// - File: an external font loaded from a .ttf file
|
||||
// - System: native laf-os fonts (i.e. Skia fonts loaded from the operating system)
|
||||
//
|
||||
// This font reference can be serialize to a string to be saved in the
|
||||
// aseprite.ini configuration (e.g. latest font used in text tool, or
|
||||
// custom theme fonts, etc.).
|
||||
class FontInfo {
|
||||
public:
|
||||
enum class Type {
|
||||
|
|
@ -41,9 +52,16 @@ public:
|
|||
const std::string& name = {},
|
||||
float size = kDefaultSize,
|
||||
text::FontStyle style = text::FontStyle(),
|
||||
Flags flags = Flags::None);
|
||||
Flags flags = Flags::None,
|
||||
text::FontHinting hinting = text::FontHinting::Normal);
|
||||
|
||||
FontInfo(const FontInfo& other, float size, text::FontStyle style, Flags flags);
|
||||
FontInfo(const FontInfo& other,
|
||||
float size,
|
||||
text::FontStyle style,
|
||||
Flags flags,
|
||||
text::FontHinting hinting);
|
||||
|
||||
FontInfo(const FontData* data, float size = 0.0f);
|
||||
|
||||
bool isValid() const { return m_type != Type::Unknown; }
|
||||
bool useDefaultSize() const { return m_size == kDefaultSize; }
|
||||
|
|
@ -64,6 +82,7 @@ public:
|
|||
Flags flags() const { return m_flags; }
|
||||
bool antialias() const;
|
||||
bool ligatures() const;
|
||||
text::FontHinting hinting() const { return m_hinting; }
|
||||
|
||||
text::TypefaceRef findTypeface(const text::FontMgrRef& fontMgr) const;
|
||||
|
||||
|
|
@ -84,6 +103,7 @@ private:
|
|||
float m_size = kDefaultSize;
|
||||
text::FontStyle m_style;
|
||||
Flags m_flags = Flags::None;
|
||||
text::FontHinting m_hinting = text::FontHinting::Normal;
|
||||
};
|
||||
|
||||
LAF_ENUM_FLAGS(FontInfo::Flags);
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
// Aseprite
|
||||
// Copyright (c) 2024 Igara Studio S.A.
|
||||
// Copyright (c) 2024-2025 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#include "tests/app_test.h"
|
||||
|
||||
#include "app/font_info.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
|
||||
using namespace app;
|
||||
using namespace std::literals;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2025 Igara Studio S.A.
|
||||
// Copyright (C) 2017-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -8,7 +9,7 @@
|
|||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/font_path.h"
|
||||
#include "app/fonts/font_path.h"
|
||||
|
||||
#include "base/fs.h"
|
||||
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_FONT_PATH_H_INCLUDED
|
||||
#define APP_FONT_PATH_H_INCLUDED
|
||||
#ifndef APP_FONTS_FONT_PATH_H_INCLUDED
|
||||
#define APP_FONTS_FONT_PATH_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "base/paths.h"
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2025 Igara Studio S.A.
|
||||
// Copyright (C) 2017-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -8,7 +9,7 @@
|
|||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/font_path.h"
|
||||
#include "app/fonts/font_path.h"
|
||||
|
||||
#include "base/fs.h"
|
||||
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2025 Igara Studio S.A.
|
||||
// Copyright (C) 2017-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -8,7 +9,7 @@
|
|||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/font_path.h"
|
||||
#include "app/fonts/font_path.h"
|
||||
|
||||
#include "base/fs.h"
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2017-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/font_path.h"
|
||||
#include "app/fonts/font_path.h"
|
||||
|
||||
#include "base/fs.h"
|
||||
#include "base/string.h"
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// 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/fonts/fonts.h"
|
||||
|
||||
#include "app/fonts/font_data.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
#include "text/font_mgr.h"
|
||||
|
||||
namespace app {
|
||||
|
||||
static Fonts* g_instance = nullptr;
|
||||
|
||||
// static
|
||||
Fonts* Fonts::instance()
|
||||
{
|
||||
ASSERT(g_instance);
|
||||
return g_instance;
|
||||
}
|
||||
|
||||
Fonts::Fonts(const text::FontMgrRef& fontMgr) : m_fontMgr(fontMgr)
|
||||
{
|
||||
ASSERT(!g_instance);
|
||||
g_instance = this;
|
||||
}
|
||||
|
||||
Fonts::~Fonts()
|
||||
{
|
||||
ASSERT(g_instance == this);
|
||||
g_instance = nullptr;
|
||||
}
|
||||
|
||||
void Fonts::addFontData(std::unique_ptr<FontData>&& fontData)
|
||||
{
|
||||
ASSERT(fontData);
|
||||
std::string name = fontData->name();
|
||||
m_fonts[name] = std::move(fontData);
|
||||
}
|
||||
|
||||
FontData* Fonts::fontDataByName(const std::string& name)
|
||||
{
|
||||
auto it = m_fonts.find(name);
|
||||
if (it == m_fonts.end())
|
||||
return nullptr;
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
text::FontRef Fonts::fontByName(const std::string& name, const float size)
|
||||
{
|
||||
auto it = m_fonts.find(name);
|
||||
if (it == m_fonts.end())
|
||||
return nullptr;
|
||||
return it->second->getFont(m_fontMgr, size);
|
||||
}
|
||||
|
||||
text::FontRef Fonts::fontFromInfo(const FontInfo& fontInfo)
|
||||
{
|
||||
ASSERT(m_fontMgr);
|
||||
if (!m_fontMgr)
|
||||
return nullptr;
|
||||
|
||||
text::FontRef font;
|
||||
if (fontInfo.type() == FontInfo::Type::System) {
|
||||
// Just in case the typeface is not present in the FontInfo
|
||||
auto typeface = fontInfo.findTypeface(m_fontMgr);
|
||||
if (!typeface)
|
||||
return nullptr;
|
||||
|
||||
if (fontInfo.useDefaultSize())
|
||||
font = m_fontMgr->makeFont(typeface);
|
||||
else
|
||||
font = m_fontMgr->makeFont(typeface, fontInfo.size());
|
||||
}
|
||||
else {
|
||||
const float size = (fontInfo.useDefaultSize() ? 0.0f : fontInfo.size());
|
||||
font = fontByName(fontInfo.name(), size);
|
||||
|
||||
if (!font && fontInfo.type() == FontInfo::Type::File) {
|
||||
font = m_fontMgr->loadTrueTypeFont(fontInfo.name().c_str(), size);
|
||||
}
|
||||
}
|
||||
|
||||
if (font) {
|
||||
font->setAntialias(fontInfo.antialias());
|
||||
font->setHinting(fontInfo.hinting());
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// 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_FONTS_FONTS_H_INCLUDED
|
||||
#define APP_FONTS_FONTS_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "text/fwd.h"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
||||
class FontData;
|
||||
using FontDataMap = std::map<std::string, std::unique_ptr<FontData>>;
|
||||
class FontInfo;
|
||||
|
||||
// Available defined fonts in "data/fonts/fonts.xml" file and theme
|
||||
// fonts (<font> elements from "data/extensions/aseprite-theme/theme.xml").
|
||||
class Fonts {
|
||||
public:
|
||||
static Fonts* instance();
|
||||
|
||||
Fonts(const text::FontMgrRef& fontMgr);
|
||||
~Fonts();
|
||||
|
||||
const text::FontMgrRef& fontMgr() const { return m_fontMgr; }
|
||||
const FontDataMap& definedFonts() const { return m_fonts; }
|
||||
bool isEmpty() const { return m_fonts.empty(); }
|
||||
|
||||
void addFontData(std::unique_ptr<FontData>&& fontData);
|
||||
|
||||
FontData* fontDataByName(const std::string& name);
|
||||
text::FontRef fontByName(const std::string& name, float size);
|
||||
text::FontRef fontFromInfo(const FontInfo& fontInfo);
|
||||
|
||||
private:
|
||||
text::FontMgrRef m_fontMgr;
|
||||
FontDataMap m_fonts;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -682,11 +682,11 @@ bool CustomizedGuiManager::processKey(Message* msg)
|
|||
{
|
||||
App* app = App::instance();
|
||||
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
||||
const KeyContext contexts[] = { keys->getCurrentKeyContext(), KeyContext::Normal };
|
||||
const KeyContext contexts[] = { KeyboardShortcuts::getCurrentKeyContext(), KeyContext::Normal };
|
||||
int n = (contexts[0] != contexts[1] ? 2 : 1);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
for (const KeyPtr& key : *keys) {
|
||||
if (key->isPressed(msg, *keys, contexts[i])) {
|
||||
if (key->isPressed(msg, contexts[i])) {
|
||||
// Cancel menu-bar loops (to close any popup menu)
|
||||
app->mainWindow()->getMenuBar()->cancelMenuLoop();
|
||||
|
||||
|
|
@ -700,7 +700,7 @@ bool CustomizedGuiManager::processKey(Message* msg)
|
|||
// Collect all tools with the pressed keyboard-shortcut
|
||||
for (tools::Tool* tool : *toolbox) {
|
||||
const KeyPtr key = keys->tool(tool);
|
||||
if (key && key->isPressed(msg, *keys))
|
||||
if (key && key->isPressed(msg))
|
||||
possibles.push_back(tool);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@
|
|||
|
||||
// Increment this value if the scripting API is modified between two
|
||||
// released Aseprite versions.
|
||||
#define API_VERSION 32
|
||||
#define API_VERSION 33
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -575,8 +575,9 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
|
|||
bool vexpand = (widget->type() == Canvas::Type());
|
||||
|
||||
// This is to separate different kind of widgets without label in
|
||||
// different rows.
|
||||
if (dlg->lastWidgetType != widget->type() || dlg->autoNewRow) {
|
||||
// different rows. Separator widgets will always create a new row.
|
||||
if (dlg->lastWidgetType != widget->type() || dlg->autoNewRow ||
|
||||
widget->type() == ui::kSeparatorWidget) {
|
||||
dlg->lastWidgetType = widget->type();
|
||||
dlg->hbox = nullptr;
|
||||
}
|
||||
|
|
@ -631,8 +632,8 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
|
|||
dlg->labelWidgets[id] = labelWidget;
|
||||
}
|
||||
else {
|
||||
// For tabs we don't want the empty space of an unspecified label.
|
||||
if (widget->type() != Tabs::Type()) {
|
||||
// For tabs and separators, we don't want the empty space of an unspecified label.
|
||||
if (widget->type() != Tabs::Type() && widget->type() != ui::kSeparatorWidget) {
|
||||
dlg->currentGrid->addChildInCell(new ui::HBox, 1, 1, ui::LEFT | ui::TOP);
|
||||
}
|
||||
}
|
||||
|
|
@ -641,14 +642,15 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
|
|||
if (widget->type() == ui::kButtonWidget)
|
||||
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.
|
||||
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,
|
||||
hspan,
|
||||
1,
|
||||
ui::HORIZONTAL | (vexpand ? ui::VERTICAL : ui::TOP));
|
||||
|
||||
dlg->hbox = hbox;
|
||||
}
|
||||
|
||||
|
|
@ -709,12 +711,7 @@ int Dialog_separator(lua_State* L)
|
|||
dlg->dataWidgets[id] = widget;
|
||||
}
|
||||
|
||||
dlg->mainWidgets.push_back(widget);
|
||||
dlg->currentGrid->addChildInCell(widget, 2, 1, ui::HORIZONTAL | ui::TOP);
|
||||
dlg->hbox = nullptr;
|
||||
|
||||
lua_pushvalue(L, 1);
|
||||
return 1;
|
||||
return Dialog_add_widget(L, widget);
|
||||
}
|
||||
|
||||
int Dialog_label(lua_State* L)
|
||||
|
|
@ -1048,7 +1045,7 @@ int Dialog_shades(lua_State* L)
|
|||
int Dialog_file(lua_State* L)
|
||||
{
|
||||
std::string title = "Open File";
|
||||
std::string path = std::string();
|
||||
std::string path;
|
||||
std::string fn;
|
||||
base::paths exts;
|
||||
auto dlgType = FileSelectorType::Open;
|
||||
|
|
@ -1114,11 +1111,14 @@ int Dialog_file(lua_State* L)
|
|||
|
||||
// Set default path if 'basepath' is blank
|
||||
if (path.empty()) {
|
||||
const auto* doc = App::instance()->context()->activeDocument();
|
||||
if (doc)
|
||||
path = base::get_file_path(doc->filename());
|
||||
else
|
||||
path = (base::get_file_path(fn).empty() ? base::get_current_path() : base::get_file_path(fn));
|
||||
// We use the 'filename' path the relative path if it was given.
|
||||
path = base::get_file_path(fn);
|
||||
if (path.empty()) {
|
||||
if (const auto* doc = App::instance()->context()->activeDocument())
|
||||
path = base::get_file_path(doc->filename());
|
||||
else
|
||||
path = base::get_current_path();
|
||||
}
|
||||
}
|
||||
|
||||
// Update the widget with the provided filename
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2015-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/cmd/clear_rect.h"
|
||||
#include "app/cmd/copy_rect.h"
|
||||
#include "app/cmd/copy_region.h"
|
||||
#include "app/cmd/flip_image.h"
|
||||
|
|
@ -246,7 +247,15 @@ int Image_clear(lua_State* L)
|
|||
else
|
||||
color = convert_args_into_pixel_color(L, i, img->pixelFormat());
|
||||
|
||||
doc::fill_rect(img, rc, color); // Clips the rectangle to the image bounds
|
||||
if (auto cel = obj->cel(L)) {
|
||||
Tx tx(cel->sprite());
|
||||
tx(new cmd::ClearRect(cel, rc.offset(cel->position()), color));
|
||||
tx.commit();
|
||||
}
|
||||
// If the destination image is not related to a sprite, we just draw
|
||||
// the source image without undo information.
|
||||
else
|
||||
doc::fill_rect(img, rc, color); // Clips the rectangle to the image bounds
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -247,6 +247,13 @@ int Layer_get_tileset(lua_State* L)
|
|||
return 1;
|
||||
}
|
||||
|
||||
int Layer_get_uuid(lua_State* L)
|
||||
{
|
||||
auto* layer = get_docobj<Layer>(L, 1);
|
||||
push_obj(L, layer->uuid());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Layer_set_name(lua_State* L)
|
||||
{
|
||||
auto layer = get_docobj<Layer>(L, 1);
|
||||
|
|
@ -446,6 +453,7 @@ const Property Layer_properties[] = {
|
|||
{ "data", UserData_get_text<Layer>, UserData_set_text<Layer> },
|
||||
{ "properties", UserData_get_properties<Layer>, UserData_set_properties<Layer> },
|
||||
{ "tileset", Layer_get_tileset, Layer_set_tileset },
|
||||
{ "uuid", Layer_get_uuid, nullptr },
|
||||
{ nullptr, nullptr, nullptr }
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ int secure_io_lines(lua_State* L);
|
|||
int secure_io_input(lua_State* L);
|
||||
int secure_io_output(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);
|
||||
|
||||
enum {
|
||||
|
|
@ -49,6 +51,8 @@ enum {
|
|||
io_input,
|
||||
io_output,
|
||||
os_execute,
|
||||
os_remove,
|
||||
os_rename,
|
||||
package_loadlib,
|
||||
};
|
||||
|
||||
|
|
@ -64,6 +68,8 @@ static struct {
|
|||
{ "io", "input", secure_io_input },
|
||||
{ "io", "output", secure_io_output },
|
||||
{ "os", "execute", secure_os_execute },
|
||||
{ "os", "remove", secure_os_remove },
|
||||
{ "os", "rename", secure_os_rename },
|
||||
{ "package", "loadlib", secure_package_loadlib },
|
||||
};
|
||||
|
||||
|
|
@ -185,6 +191,81 @@ int secure_os_execute(lua_State* 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 (std::exception& e) {
|
||||
return file_result(L, false, EIO, absFilename);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
base::delete_file(absFilename);
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
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 (std::exception& e) {
|
||||
return file_result(L, false, EIO, absSourceFilename);
|
||||
}
|
||||
}
|
||||
|
||||
int secure_package_loadlib(lua_State* L)
|
||||
{
|
||||
const char* cmd = luaL_checkstring(L, 1);
|
||||
|
|
@ -201,7 +282,7 @@ void overwrite_unsecure_functions(lua_State* L)
|
|||
{
|
||||
// Remove unsupported functions
|
||||
lua_getglobal(L, "os");
|
||||
for (const char* name : { "remove", "rename", "exit", "tmpname" }) {
|
||||
for (const char* name : { "exit", "tmpname" }) {
|
||||
lua_pushcfunction(L, unsupported);
|
||||
lua_setfield(L, -2, name);
|
||||
}
|
||||
|
|
@ -280,7 +361,15 @@ bool ask_access(lua_State* L,
|
|||
{
|
||||
std::string label;
|
||||
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::WebSocket: label = Strings::script_access_websocket_label(); break;
|
||||
case ResourceType::Clipboard: label = Strings::script_access_clipboard_label(); break;
|
||||
|
|
|
|||
|
|
@ -1012,6 +1012,23 @@ int Sprite_set_tileManagementPlugin(lua_State* L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int Sprite_get_useLayerUuids(lua_State* L)
|
||||
{
|
||||
auto* sprite = get_docobj<Sprite>(L, 1);
|
||||
lua_pushboolean(L, sprite->useLayerUuids());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Sprite_set_useLayerUuids(lua_State* L)
|
||||
{
|
||||
auto* sprite = get_docobj<Sprite>(L, 1);
|
||||
if (lua_isboolean(L, 2)) {
|
||||
const bool value = lua_toboolean(L, 2);
|
||||
sprite->useLayerUuids(value);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const luaL_Reg Sprite_methods[] = {
|
||||
{ "__eq", Sprite_eq },
|
||||
{ "resize", Sprite_resize },
|
||||
|
|
@ -1076,6 +1093,7 @@ const Property Sprite_properties[] = {
|
|||
{ "pixelRatio", Sprite_get_pixelRatio, Sprite_set_pixelRatio },
|
||||
{ "events", Sprite_get_events, nullptr },
|
||||
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
|
||||
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
|
||||
{ nullptr, nullptr, nullptr }
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,17 @@
|
|||
|
||||
namespace app { namespace thumb {
|
||||
|
||||
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel, const gfx::Size& fitInSize)
|
||||
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
|
||||
const bool scaleUpToFit,
|
||||
const gfx::Size& fitInSize)
|
||||
{
|
||||
gfx::Size newSize(gfx::Rect(cel->bounds()).fitIn(gfx::Rect(fitInSize)).size());
|
||||
gfx::Size newSize;
|
||||
|
||||
if (scaleUpToFit || cel->bounds().w > fitInSize.w || cel->bounds().h > fitInSize.h)
|
||||
newSize = gfx::Rect(cel->bounds()).fitIn(gfx::Rect(fitInSize)).size();
|
||||
else
|
||||
newSize = cel->bounds().size();
|
||||
|
||||
if (newSize.w < 1 || newSize.h < 1)
|
||||
return nullptr;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2020 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2016 Carlo Caputo
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -22,7 +22,9 @@ class Surface;
|
|||
|
||||
namespace app { namespace thumb {
|
||||
|
||||
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel, const gfx::Size& fitInSize);
|
||||
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
|
||||
const bool scaleUpToFit,
|
||||
const gfx::Size& fitInSize);
|
||||
|
||||
}} // namespace app::thumb
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2021-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2021-2025 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
|
@ -20,7 +20,7 @@ void Symmetry::generateStrokes(const Stroke& stroke, Strokes& strokes, ToolLoop*
|
|||
{
|
||||
Stroke stroke2;
|
||||
strokes.push_back(stroke);
|
||||
gen::SymmetryMode symmetryMode = loop->getSymmetry()->mode();
|
||||
const gen::SymmetryMode symmetryMode = tools::Symmetry::resolveMode(loop->getSymmetry()->mode());
|
||||
switch (symmetryMode) {
|
||||
case gen::SymmetryMode::NONE: ASSERT(false); break;
|
||||
|
||||
|
|
@ -171,4 +171,14 @@ void Symmetry::calculateSymmetricalStroke(const Stroke& refStroke,
|
|||
}
|
||||
}
|
||||
|
||||
gen::SymmetryMode Symmetry::resolveMode(gen::SymmetryMode mode)
|
||||
{
|
||||
return (((int(mode) & int(gen::SymmetryMode::HORIZONTAL)) ||
|
||||
(int(mode) & int(gen::SymmetryMode::VERTICAL))) &&
|
||||
((int(mode) & int(gen::SymmetryMode::RIGHT_DIAG)) ||
|
||||
(int(mode) & int(gen::SymmetryMode::LEFT_DIAG)))) ?
|
||||
gen::SymmetryMode::ALL :
|
||||
mode;
|
||||
}
|
||||
|
||||
}} // namespace app::tools
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2021-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2021-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2015 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -38,6 +38,8 @@ public:
|
|||
|
||||
gen::SymmetryMode mode() const { return m_symmetryMode; }
|
||||
|
||||
static gen::SymmetryMode resolveMode(gen::SymmetryMode mode);
|
||||
|
||||
private:
|
||||
void calculateSymmetricalStroke(const Stroke& refStroke,
|
||||
Stroke& stroke,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -19,9 +19,10 @@
|
|||
#include "app/ui/keyboard_shortcuts.h"
|
||||
#include "app/ui_context.h"
|
||||
#include "os/menus.h"
|
||||
#include "ui/accelerator.h"
|
||||
#include "ui/menu.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/scale.h"
|
||||
#include "ui/shortcut.h"
|
||||
#include "ui/size_hint_event.h"
|
||||
#include "ui/widget.h"
|
||||
|
||||
|
|
@ -134,12 +135,12 @@ void AppMenuItem::onSizeHint(SizeHintEvent& ev)
|
|||
gfx::Size size(0, 0);
|
||||
|
||||
if (hasText()) {
|
||||
size.w = +textWidth() + (inBar() ? childSpacing() / 4 : childSpacing()) + border().width();
|
||||
size.w = textWidth() + (inBar() ? guiscaled_div(childSpacing(), 4) : childSpacing()) +
|
||||
border().width();
|
||||
size.h = textHeight() + border().height();
|
||||
|
||||
size.h = +textHeight() + border().height();
|
||||
|
||||
if (m_key && !m_key->accels().empty()) {
|
||||
size.w += font()->textLength(m_key->accels().front().toString());
|
||||
if (m_key && !m_key->shortcuts().empty()) {
|
||||
size.w += font()->textLength(m_key->shortcuts().front().toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2016-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -65,7 +65,7 @@ private:
|
|||
class BrowserView::CMarkBox : public Widget {
|
||||
class Break : public Widget {
|
||||
public:
|
||||
Break() { setMinSize(gfx::Size(0, font()->height())); }
|
||||
Break() { setMinSize(gfx::Size(0, font()->lineHeight())); }
|
||||
};
|
||||
class OpenList : public Widget {};
|
||||
class CloseList : public Widget {};
|
||||
|
|
@ -439,7 +439,8 @@ private:
|
|||
void addSeparator()
|
||||
{
|
||||
auto sep = new SeparatorInView(std::string(), HORIZONTAL);
|
||||
sep->setBorder(gfx::Border(0, font()->height(), 0, font()->height()));
|
||||
float h = font()->lineHeight() / 2.0f;
|
||||
sep->setBorder(gfx::Border(0, h, 0, h));
|
||||
sep->setExpansive(true);
|
||||
addChild(sep);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -431,8 +431,8 @@ void BrushPopup::regenerate(ui::Display* display, const gfx::Point& pos)
|
|||
params.set("change", "custom");
|
||||
params.set("slot", base::convert_to<std::string>(slot).c_str());
|
||||
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
|
||||
if (key && !key->accels().empty())
|
||||
shortcut = key->accels().front().toString();
|
||||
if (key && !key->shortcuts().empty())
|
||||
shortcut = key->shortcuts().front().toString();
|
||||
}
|
||||
m_customBrushes->addItem(new SelectBrushItem(brush, slot));
|
||||
m_customBrushes->addItem(new BrushShortcutItem(shortcut, slot));
|
||||
|
|
@ -514,7 +514,7 @@ os::SurfaceRef BrushPopup::createSurfaceForBrush(const BrushRef& origBrush,
|
|||
if (image->pixelFormat() == IMAGE_BITMAP)
|
||||
delete palette;
|
||||
|
||||
surface->applyScale(guiscale());
|
||||
surface = surface->applyScale(guiscale());
|
||||
}
|
||||
else {
|
||||
surface->clear();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2023 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -287,6 +287,7 @@ void ColorShades::onPaint(ui::PaintEvent& ev)
|
|||
ui::PaintWidgetPartInfo info;
|
||||
const std::string& text = this->text();
|
||||
info.text = &text;
|
||||
info.baseline = textBaseline();
|
||||
theme->paintWidgetPart(g, theme->styles.shadeEmpty(), bounds, info);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2023 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -391,7 +391,8 @@ void ColorSliders::addSlider(const Channel channel,
|
|||
gfx::Size sz(std::numeric_limits<int>::max(), theme->dimensions.colorSliderHeight());
|
||||
item.label->setMaxSize(sz);
|
||||
item.box->setMaxSize(sz);
|
||||
item.entry->setMaxSize(sz);
|
||||
// Don't limit the entry size as it will be too small for UI Scaling=200%
|
||||
// item.entry->setMaxSize(sz);
|
||||
|
||||
m_grid.addChildInCell(item.label, 1, 1, LEFT | MIDDLE);
|
||||
m_grid.addChildInCell(item.box, 1, 1, HORIZONTAL | VERTICAL);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2020-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -69,10 +69,13 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
|
|||
m_box->thumbEnabled()->Click.connect([this] { onThumbEnabledChange(); });
|
||||
m_box->thumbOverlayEnabled()->Click.connect([this] { onThumbOverlayEnabledChange(); });
|
||||
m_box->thumbOverlaySize()->Change.connect([this] { onThumbOverlaySizeChange(); });
|
||||
m_box->thumbScaleUpToFit()->Click.connect([this] { onScaleUpToFitChange(); });
|
||||
|
||||
const bool visibleThumb = docPref().thumbnails.enabled();
|
||||
m_box->thumbHSeparator()->setVisible(visibleThumb);
|
||||
m_box->thumbBox()->setVisible(visibleThumb);
|
||||
|
||||
m_box->defaults()->Click.connect([this] { onSetAsDefaults(); });
|
||||
}
|
||||
|
||||
Doc* ConfigureTimelinePopup::doc()
|
||||
|
|
@ -128,6 +131,7 @@ void ConfigureTimelinePopup::updateWidgetsFromCurrentSettings()
|
|||
m_box->thumbBox()->setVisible(visibleThumb);
|
||||
m_box->thumbOverlayEnabled()->setSelected(docPref.thumbnails.overlayEnabled());
|
||||
m_box->thumbOverlaySize()->setValue(docPref.thumbnails.overlaySize());
|
||||
m_box->thumbScaleUpToFit()->setSelected(docPref.thumbnails.scaleUpToFit());
|
||||
|
||||
expandWindow(sizeHint());
|
||||
}
|
||||
|
|
@ -237,4 +241,33 @@ void ConfigureTimelinePopup::onThumbOverlaySizeChange()
|
|||
docPref().thumbnails.overlaySize(m_box->thumbOverlaySize()->getValue());
|
||||
}
|
||||
|
||||
void ConfigureTimelinePopup::onScaleUpToFitChange()
|
||||
{
|
||||
docPref().thumbnails.scaleUpToFit(m_box->thumbScaleUpToFit()->isSelected());
|
||||
}
|
||||
|
||||
void ConfigureTimelinePopup::onSetAsDefaults()
|
||||
{
|
||||
const auto& docPref = this->docPref();
|
||||
auto& defaults = Preferences::instance().document(nullptr);
|
||||
|
||||
defaults.timeline.firstFrame(docPref.timeline.firstFrame());
|
||||
|
||||
defaults.thumbnails.enabled(docPref.thumbnails.enabled());
|
||||
defaults.thumbnails.zoom(docPref.thumbnails.zoom());
|
||||
defaults.thumbnails.overlayEnabled(docPref.thumbnails.overlayEnabled());
|
||||
defaults.thumbnails.overlaySize(docPref.thumbnails.overlaySize());
|
||||
defaults.thumbnails.scaleUpToFit(docPref.thumbnails.scaleUpToFit());
|
||||
|
||||
defaults.onionskin.active(docPref.onionskin.active());
|
||||
defaults.onionskin.prevFrames(docPref.onionskin.prevFrames());
|
||||
defaults.onionskin.nextFrames(docPref.onionskin.nextFrames());
|
||||
defaults.onionskin.opacityBase(docPref.onionskin.opacityBase());
|
||||
defaults.onionskin.opacityStep(docPref.onionskin.opacityStep());
|
||||
defaults.onionskin.type(docPref.onionskin.type());
|
||||
defaults.onionskin.loopTag(docPref.onionskin.loopTag());
|
||||
defaults.onionskin.currentLayer(docPref.onionskin.currentLayer());
|
||||
defaults.onionskin.position(docPref.onionskin.position());
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -46,6 +47,8 @@ protected:
|
|||
void onThumbEnabledChange();
|
||||
void onThumbOverlayEnabledChange();
|
||||
void onThumbOverlaySizeChange();
|
||||
void onScaleUpToFitChange();
|
||||
void onSetAsDefaults();
|
||||
|
||||
private:
|
||||
void updateWidgetsFromCurrentSettings();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -1530,14 +1530,10 @@ public:
|
|||
|
||||
DocumentPreferences& docPref = Preferences::instance().document(doc);
|
||||
|
||||
at(0)->setSelected(
|
||||
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::HORIZONTAL) ? true : false);
|
||||
at(1)->setSelected(
|
||||
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::VERTICAL) ? true : false);
|
||||
at(2)->setSelected(
|
||||
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::RIGHT_DIAG) ? true : false);
|
||||
at(3)->setSelected(
|
||||
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::LEFT_DIAG) ? true : false);
|
||||
at(0)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::HORIZONTAL));
|
||||
at(1)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::VERTICAL));
|
||||
at(2)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::RIGHT_DIAG));
|
||||
at(3)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::LEFT_DIAG));
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
@ -1551,60 +1547,7 @@ private:
|
|||
|
||||
DocumentPreferences& docPref = Preferences::instance().document(doc);
|
||||
|
||||
auto oldMode = docPref.symmetry.mode();
|
||||
int mode = 0;
|
||||
if (at(0)->isSelected())
|
||||
mode |= int(app::gen::SymmetryMode::HORIZONTAL);
|
||||
if (at(1)->isSelected())
|
||||
mode |= int(app::gen::SymmetryMode::VERTICAL);
|
||||
if (at(2)->isSelected())
|
||||
mode |= int(app::gen::SymmetryMode::RIGHT_DIAG);
|
||||
if (at(3)->isSelected())
|
||||
mode |= int(app::gen::SymmetryMode::LEFT_DIAG);
|
||||
|
||||
// Non sense symmetries filter:
|
||||
// - H + 1Diag
|
||||
// - V + 1Diag
|
||||
// - H + V + 1Diag
|
||||
const bool HorV = (mode & int(app::gen::SymmetryMode::HORIZONTAL)) ||
|
||||
(mode & int(app::gen::SymmetryMode::VERTICAL));
|
||||
const bool HxorV = !(mode & int(app::gen::SymmetryMode::HORIZONTAL)) !=
|
||||
!(mode & int(app::gen::SymmetryMode::VERTICAL));
|
||||
const bool RDxorLD = !(mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) !=
|
||||
!(mode & int(app::gen::SymmetryMode::LEFT_DIAG));
|
||||
if (oldMode == gen::SymmetryMode::HORIZONTAL || oldMode == gen::SymmetryMode::VERTICAL ||
|
||||
oldMode == gen::SymmetryMode::BOTH) {
|
||||
if (HorV && RDxorLD) {
|
||||
mode = int(app::gen::SymmetryMode::ALL);
|
||||
at(0)->setSelected(true);
|
||||
at(1)->setSelected(true);
|
||||
at(2)->setSelected(true);
|
||||
at(3)->setSelected(true);
|
||||
}
|
||||
}
|
||||
else if (oldMode == gen::SymmetryMode::ALL) {
|
||||
if (HxorV) {
|
||||
mode = int(app::gen::SymmetryMode::BOTH_DIAG);
|
||||
at(0)->setSelected(false);
|
||||
at(1)->setSelected(false);
|
||||
}
|
||||
else if (RDxorLD) {
|
||||
mode = int(app::gen::SymmetryMode::BOTH);
|
||||
at(2)->setSelected(false);
|
||||
at(3)->setSelected(false);
|
||||
}
|
||||
}
|
||||
else if ((oldMode == gen::SymmetryMode::RIGHT_DIAG || oldMode == gen::SymmetryMode::LEFT_DIAG ||
|
||||
oldMode == gen::SymmetryMode::BOTH_DIAG) &&
|
||||
HorV) {
|
||||
mode = int(app::gen::SymmetryMode::ALL);
|
||||
at(0)->setSelected(true);
|
||||
at(1)->setSelected(true);
|
||||
at(2)->setSelected(true);
|
||||
at(3)->setSelected(true);
|
||||
}
|
||||
// Non sense symmetries filter end
|
||||
|
||||
if (at(0)->isSelected())
|
||||
mode |= int(app::gen::SymmetryMode::HORIZONTAL);
|
||||
if (at(1)->isSelected())
|
||||
|
|
|
|||
|
|
@ -180,7 +180,6 @@ DitheringSelector::DitheringSelector(Type type) : m_type(type)
|
|||
m_extChanges = extensions.DitheringMatricesChange.connect([this] { regenerate(); });
|
||||
|
||||
setUseCustomWidget(true);
|
||||
regenerate();
|
||||
}
|
||||
|
||||
void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
|
||||
|
|
@ -190,6 +189,16 @@ void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
|
|||
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)
|
||||
{
|
||||
int index = findItemIndex(name);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public:
|
|||
|
||||
protected:
|
||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||
void onVisible(bool visible) override;
|
||||
|
||||
private:
|
||||
void regenerate(int selectedItemIndex = 0);
|
||||
|
|
@ -38,6 +39,7 @@ private:
|
|||
|
||||
Type m_type;
|
||||
obs::scoped_connection m_extChanges;
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -41,10 +41,10 @@
|
|||
#include "doc/layer.h"
|
||||
#include "doc/sprite.h"
|
||||
#include "fmt/format.h"
|
||||
#include "ui/accelerator.h"
|
||||
#include "ui/alert.h"
|
||||
#include "ui/menu.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/shortcut.h"
|
||||
#include "ui/system.h"
|
||||
#include "ui/view.h"
|
||||
|
||||
|
|
@ -147,11 +147,11 @@ protected:
|
|||
KeyPtr rmb = keys->action(KeyAction::RightMouseButton, KeyContext::Any);
|
||||
|
||||
// Convert action keys into mouse messages.
|
||||
if (lmb->isPressed(msg, *keys) || rmb->isPressed(msg, *keys)) {
|
||||
if (lmb->isPressed(msg) || rmb->isPressed(msg)) {
|
||||
MouseMessage mouseMsg(
|
||||
(msg->type() == kKeyDownMessage ? kMouseDownMessage : kMouseUpMessage),
|
||||
PointerType::Unknown,
|
||||
(lmb->isPressed(msg, *keys) ? kButtonLeft : kButtonRight),
|
||||
(lmb->isPressed(msg) ? kButtonLeft : kButtonRight),
|
||||
msg->modifiers(),
|
||||
mousePosInDisplay());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2020-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
|
@ -131,11 +131,12 @@ private:
|
|||
theme->parts.miniSliderFull().get());
|
||||
}
|
||||
|
||||
const int sensorH = guiscaled_div(rc.h, 4);
|
||||
g->fillRect(theme->colors.sliderEmptyText(),
|
||||
gfx::Rect(rc.x, rc.y + rc.h / 2 - rc.h / 8, sensorW, rc.h / 4));
|
||||
gfx::Rect(rc.x, guiscaled_center(rc.y, rc.h, sensorH), sensorW, sensorH));
|
||||
|
||||
g->drawRgbaSurface(thumb, minX - thumb->width() / 2, thumb_y);
|
||||
g->drawRgbaSurface(thumb, maxX - thumb->width() / 2, thumb_y);
|
||||
g->drawRgbaSurface(thumb, minX - guiscaled_div(thumb->width(), 2), thumb_y);
|
||||
g->drawRgbaSurface(thumb, maxX - guiscaled_div(thumb->width(), 2), thumb_y);
|
||||
}
|
||||
|
||||
bool onProcessMessage(Message* msg) override
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
|
@ -25,10 +25,10 @@ DelayedMouseMove::DelayedMouseMove(DelayedMouseMoveDelegate* delegate,
|
|||
: m_delegate(delegate)
|
||||
, m_editor(editor)
|
||||
, m_timer(interval)
|
||||
, m_spritePos(std::numeric_limits<float>::min(), std::numeric_limits<float>::min())
|
||||
, m_mouseMoveReceived(false)
|
||||
, m_mouseDownPos(kNoPosReceived)
|
||||
, m_mouseDownTime(base::current_tick())
|
||||
, m_spritePos(std::numeric_limits<float>::min(), std::numeric_limits<float>::min())
|
||||
{
|
||||
ASSERT(m_delegate);
|
||||
m_timer.Tick.connect([this] { commitMouseMove(); });
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
#include "app/tools/active_tool.h"
|
||||
#include "app/tools/controller.h"
|
||||
#include "app/tools/ink.h"
|
||||
#include "app/tools/symmetry.h"
|
||||
#include "app/tools/tool.h"
|
||||
#include "app/tools/tool_box.h"
|
||||
#include "app/ui/color_bar.h"
|
||||
|
|
@ -836,6 +837,86 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g,
|
|||
m_docPref.grid.forceSection();
|
||||
}
|
||||
m_docPref.show.grid.forceDirtyFlag();
|
||||
|
||||
// Symmetry mode
|
||||
if (isActive() && (m_flags & Editor::kShowSymmetryLine) &&
|
||||
Preferences::instance().symmetryMode.enabled()) {
|
||||
const int symmetryButtons = int(m_docPref.symmetry.mode());
|
||||
// Symmetry::resolveMode is to calculate the right symmetry
|
||||
// mode. This is necessary because some symmetry settings
|
||||
// do not make sense and should be forced to 'ALL'
|
||||
const int mode = int(tools::Symmetry::resolveMode(m_docPref.symmetry.mode()));
|
||||
const gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
|
||||
const gfx::Color semiTransparentColor =
|
||||
gfx::rgba(rgba_getr(color), rgba_getg(color), rgba_getb(color), rgba_geta(color) / 4);
|
||||
const double x = int(m_proj.applyX<double>(m_docPref.symmetry.xAxis()));
|
||||
const double y = int(m_proj.applyY<double>(m_docPref.symmetry.yAxis()));
|
||||
|
||||
if (mode & int(app::gen::SymmetryMode::HORIZONTAL) && x > 0) {
|
||||
g->drawVLine(symmetryButtons & int(app::gen::SymmetryMode::HORIZONTAL) ?
|
||||
color :
|
||||
semiTransparentColor,
|
||||
enclosingRect.x + x,
|
||||
enclosingRect.y,
|
||||
enclosingRect.h);
|
||||
}
|
||||
if (mode & int(app::gen::SymmetryMode::VERTICAL) && y > 0) {
|
||||
g->drawHLine(
|
||||
symmetryButtons & int(app::gen::SymmetryMode::VERTICAL) ? color : semiTransparentColor,
|
||||
enclosingRect.x,
|
||||
enclosingRect.y + y,
|
||||
enclosingRect.w);
|
||||
}
|
||||
if (mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) {
|
||||
// Bottom point intersection:
|
||||
gfx::Point bottomLeft(
|
||||
enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(y - enclosingRect.h),
|
||||
enclosingRect.y2());
|
||||
if (bottomLeft.x < enclosingRect.x) {
|
||||
// Left intersection
|
||||
bottomLeft.y = enclosingRect.y2() +
|
||||
m_proj.turnXinTermsOfY<int>(bottomLeft.x - enclosingRect.x);
|
||||
bottomLeft.x = enclosingRect.x;
|
||||
}
|
||||
// Top intersection
|
||||
gfx::Point topRight(enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(y),
|
||||
enclosingRect.y);
|
||||
if (enclosingRect.x2() < topRight.x) {
|
||||
// Right intersection
|
||||
topRight.y = enclosingRect.y +
|
||||
m_proj.applyY<int>(m_proj.removeX<int>(topRight.x - enclosingRect.x2()));
|
||||
topRight.x = enclosingRect.x2();
|
||||
}
|
||||
g->drawLine(symmetryButtons & int(app::gen::SymmetryMode::RIGHT_DIAG) ?
|
||||
color :
|
||||
semiTransparentColor,
|
||||
bottomLeft,
|
||||
topRight);
|
||||
}
|
||||
if (mode & int(app::gen::SymmetryMode::LEFT_DIAG)) {
|
||||
// Bottom point intersection:
|
||||
gfx::Point bottomRight(
|
||||
enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(enclosingRect.h - y),
|
||||
enclosingRect.y2());
|
||||
if (enclosingRect.x2() < bottomRight.x) {
|
||||
// Left intersection
|
||||
bottomRight.y = enclosingRect.y2() +
|
||||
m_proj.turnXinTermsOfY<int>(enclosingRect.x2() - bottomRight.x);
|
||||
bottomRight.x = enclosingRect.x2();
|
||||
}
|
||||
// Top intersection
|
||||
gfx::Point topLeft(enclosingRect.x + x - m_proj.turnYinTermsOfX<int>(y), enclosingRect.y);
|
||||
if (topLeft.x < enclosingRect.x) {
|
||||
// Right intersection
|
||||
topLeft.y = enclosingRect.y + m_proj.turnXinTermsOfY<int>(enclosingRect.x - topLeft.x);
|
||||
topLeft.x = enclosingRect.x;
|
||||
}
|
||||
g->drawLine(
|
||||
symmetryButtons & int(app::gen::SymmetryMode::LEFT_DIAG) ? color : semiTransparentColor,
|
||||
topLeft,
|
||||
bottomRight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -908,86 +989,6 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
|
|||
enclosingRect = gfx::Rect(spriteRect.x, spriteRect.y, spriteRect.w * 3, spriteRect.h * 3);
|
||||
}
|
||||
|
||||
// Symmetry mode
|
||||
if (isActive() && (m_flags & Editor::kShowSymmetryLine) &&
|
||||
Preferences::instance().symmetryMode.enabled()) {
|
||||
int mode = int(m_docPref.symmetry.mode());
|
||||
if (mode & int(app::gen::SymmetryMode::HORIZONTAL)) {
|
||||
double x = m_docPref.symmetry.xAxis();
|
||||
if (x > 0) {
|
||||
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
|
||||
g->drawVLine(
|
||||
color,
|
||||
spriteRect.x + m_proj.applyX(mainTilePosition().x) + int(m_proj.applyX<double>(x)),
|
||||
enclosingRect.y,
|
||||
enclosingRect.h);
|
||||
}
|
||||
}
|
||||
if (mode & int(app::gen::SymmetryMode::VERTICAL)) {
|
||||
double y = m_docPref.symmetry.yAxis();
|
||||
if (y > 0) {
|
||||
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
|
||||
g->drawHLine(
|
||||
color,
|
||||
enclosingRect.x,
|
||||
spriteRect.y + m_proj.applyY(mainTilePosition().y) + int(m_proj.applyY<double>(y)),
|
||||
enclosingRect.w);
|
||||
}
|
||||
}
|
||||
if (mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) {
|
||||
double y = m_docPref.symmetry.yAxis();
|
||||
double x = m_docPref.symmetry.xAxis();
|
||||
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
|
||||
// Bottom point intersection:
|
||||
gfx::Point bottomLeft(
|
||||
enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x)) -
|
||||
(enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyY<double>(y))),
|
||||
enclosingRect.y2());
|
||||
if (bottomLeft.x < enclosingRect.x) {
|
||||
// Left intersection
|
||||
bottomLeft.y = enclosingRect.y2() - enclosingRect.x + bottomLeft.x;
|
||||
bottomLeft.x = enclosingRect.x;
|
||||
}
|
||||
// Top intersection
|
||||
gfx::Point topRight(enclosingRect.x + m_proj.applyY(mainTilePosition().x) +
|
||||
int(m_proj.applyX<double>(x)) + m_proj.applyY(mainTilePosition().y) +
|
||||
int(m_proj.applyY<double>(y)),
|
||||
enclosingRect.y);
|
||||
if (enclosingRect.x2() < topRight.x) {
|
||||
// Right intersection
|
||||
topRight.y = enclosingRect.y + topRight.x - enclosingRect.x2();
|
||||
topRight.x = enclosingRect.x2();
|
||||
}
|
||||
g->drawLine(color, bottomLeft, topRight);
|
||||
}
|
||||
if (mode & int(app::gen::SymmetryMode::LEFT_DIAG)) {
|
||||
double y = m_docPref.symmetry.yAxis();
|
||||
double x = m_docPref.symmetry.xAxis();
|
||||
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
|
||||
// Bottom point intersection:
|
||||
gfx::Point bottomRight(
|
||||
enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x)) +
|
||||
(enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyX<double>(y))),
|
||||
enclosingRect.y2());
|
||||
if (enclosingRect.x2() < bottomRight.x) {
|
||||
// Left intersection
|
||||
bottomRight.y = enclosingRect.y2() - bottomRight.x + enclosingRect.x2();
|
||||
bottomRight.x = enclosingRect.x2();
|
||||
}
|
||||
// Top intersection
|
||||
gfx::Point topLeft(enclosingRect.x + m_proj.applyY(mainTilePosition().x) +
|
||||
int(m_proj.applyX<double>(x)) - m_proj.applyY(mainTilePosition().y) -
|
||||
int(m_proj.applyY<double>(y)),
|
||||
enclosingRect.y);
|
||||
if (topLeft.x < enclosingRect.x) {
|
||||
// Right intersection
|
||||
topLeft.y = enclosingRect.y + enclosingRect.x - topLeft.x;
|
||||
topLeft.x = enclosingRect.x;
|
||||
}
|
||||
g->drawLine(color, topLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw active layer/cel edges
|
||||
if ((m_docPref.show.layerEdges() || m_showAutoCelGuides) &&
|
||||
// Show layer edges and possibly cel guides only on states that
|
||||
|
|
@ -1239,9 +1240,9 @@ void Editor::drawTileNumbers(ui::Graphics* g, const Cel* cel)
|
|||
const text::FontRef& font = g->font();
|
||||
const doc::Grid grid = getSite().grid();
|
||||
const gfx::Size tileSize = editorToScreen(grid.tileToCanvas(gfx::Rect(0, 0, 1, 1))).size();
|
||||
const int th = font->height();
|
||||
const int th = font->lineHeight();
|
||||
if (tileSize.h > th) {
|
||||
const gfx::Point offset = gfx::Point(tileSize.w / 2, tileSize.h / 2 - font->height() / 2) +
|
||||
const gfx::Point offset = gfx::Point(tileSize.w / 2, tileSize.h / 2 - font->size() / 2) +
|
||||
mainTilePosition();
|
||||
|
||||
int ti_offset = static_cast<LayerTilemap*>(cel->layer())->tileset()->baseIndex() - 1;
|
||||
|
|
@ -2452,8 +2453,10 @@ void Editor::onTiledModeChange()
|
|||
screenPos = editorToScreen(spritePos);
|
||||
|
||||
auto lastPoint = document()->lastDrawingPoint();
|
||||
lastPoint += mainTilePosition() - m_oldMainTilePos;
|
||||
document()->setLastDrawingPoint(lastPoint);
|
||||
if (lastPoint != Doc::NoLastDrawingPoint()) {
|
||||
lastPoint += mainTilePosition() - m_oldMainTilePos;
|
||||
document()->setLastDrawingPoint(lastPoint);
|
||||
}
|
||||
|
||||
centerInSpritePoint(spritePos);
|
||||
}
|
||||
|
|
@ -2536,8 +2539,6 @@ void Editor::onBeforeLayerEditableChange(DocEvent& ev, bool newState)
|
|||
|
||||
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
|
||||
{
|
||||
Rect vp = View::getView(this)->viewportBounds();
|
||||
|
||||
bool used = false;
|
||||
if (m_sprite)
|
||||
used = m_state->onSetCursor(this, mouseDisplayPos);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
|
@ -11,7 +11,8 @@
|
|||
#include "app/ui/editor/select_text_box_state.h"
|
||||
|
||||
#include "app/app.h"
|
||||
#include "app/font_info.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
#include "app/fonts/fonts.h"
|
||||
#include "app/ui/context_bar.h"
|
||||
#include "app/ui/editor/editor.h"
|
||||
#include "app/ui/editor/writing_text_state.h"
|
||||
|
|
@ -62,9 +63,9 @@ void SelectTextBoxState::onQuickboxEnd(Editor* editor, const gfx::Rect& rect0, u
|
|||
gfx::Rect rect = rect0;
|
||||
if (rect.w <= 3 || rect.h <= 3) {
|
||||
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
|
||||
if (auto font = get_font_from_info(fontInfo)) {
|
||||
rect.w = std::min(4 * font->height(), editor->sprite()->width());
|
||||
rect.h = font->height();
|
||||
if (auto font = Fonts::instance()->fontFromInfo(fontInfo)) {
|
||||
rect.w = std::min<float>(4 * std::ceil(font->size()), editor->sprite()->width());
|
||||
rect.h = std::ceil(font->size());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -464,8 +464,7 @@ bool StandbyState::onKeyDown(Editor* editor, KeyMessage* msg)
|
|||
checkStartDrawingStraightLine(editor, nullptr, nullptr))
|
||||
return false;
|
||||
|
||||
Keys keys = KeyboardShortcuts::instance()->getDragActionsFromKeyMessage(KeyContext::MouseWheel,
|
||||
msg);
|
||||
Keys keys = KeyboardShortcuts::instance()->getDragActionsFromKeyMessage(msg);
|
||||
if (editor->hasMouse() && !keys.empty()) {
|
||||
// Don't enter DraggingValueState to change brush size if we are
|
||||
// in a selection-like tool
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
#include "app/color_utils.h"
|
||||
#include "app/commands/command.h"
|
||||
#include "app/extra_cel.h"
|
||||
#include "app/font_info.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/site.h"
|
||||
#include "app/tx.h"
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
#include "render/dithering.h"
|
||||
#include "render/quantization.h"
|
||||
#include "render/render.h"
|
||||
#include "text/font_metrics.h"
|
||||
#include "ui/entry.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/paint_event.h"
|
||||
|
|
@ -63,7 +64,7 @@ public:
|
|||
renderExtraCelBase();
|
||||
|
||||
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
|
||||
if (auto font = get_font_from_info(fontInfo))
|
||||
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
|
||||
setFont(font);
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +134,13 @@ private:
|
|||
return Entry::onProcessMessage(msg);
|
||||
}
|
||||
|
||||
float onGetTextBaseline() const override
|
||||
{
|
||||
text::FontMetrics metrics;
|
||||
font()->metrics(&metrics);
|
||||
return scale().y * -metrics.ascent;
|
||||
}
|
||||
|
||||
void onInitTheme(InitThemeEvent& ev) override
|
||||
{
|
||||
Entry::onInitTheme(ev);
|
||||
|
|
@ -497,7 +505,7 @@ void WritingTextState::onBeforeCommandExecution(CommandExecutionEvent& ev)
|
|||
|
||||
void WritingTextState::onFontChange(const FontInfo& fontInfo, FontEntry::From fromField)
|
||||
{
|
||||
if (auto font = get_font_from_info(fontInfo)) {
|
||||
if (auto font = Fonts::instance()->fontFromInfo(fontInfo)) {
|
||||
m_entry->setFont(font);
|
||||
m_entry->invalidate();
|
||||
m_editor->invalidate();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2018 David Capello
|
||||
//
|
||||
// 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));
|
||||
|
||||
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(
|
||||
base::get_file_path(newFn),
|
||||
base::get_file_title(newFn) + "-export." + base::get_file_extension(newFn));
|
||||
|
|
|
|||
|
|
@ -408,7 +408,8 @@ bool FileSelector::show(const std::string& title,
|
|||
|
||||
remapWindow();
|
||||
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
|
||||
std::string initialExtension = base::get_file_extension(initialPath);
|
||||
|
|
|
|||
|
|
@ -15,15 +15,20 @@
|
|||
#include "app/recent_files.h"
|
||||
#include "app/ui/font_popup.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "base/contains.h"
|
||||
#include "base/scoped_value.h"
|
||||
#include "fmt/format.h"
|
||||
#include "ui/display.h"
|
||||
#include "ui/fit_bounds.h"
|
||||
#include "ui/manager.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/scale.h"
|
||||
|
||||
#include "font_style.xml.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
namespace app {
|
||||
|
||||
|
|
@ -211,8 +216,33 @@ void FontEntry::FontFace::onCloseIconPressed()
|
|||
FontEntry::FontSize::FontSize()
|
||||
{
|
||||
setEditable(true);
|
||||
for (int i : { 8, 9, 10, 11, 12, 14, 16, 18, 22, 24, 26, 28, 36, 48, 72 })
|
||||
addItem(fmt::format("{}", i));
|
||||
}
|
||||
|
||||
void FontEntry::FontSize::updateForFont(const FontInfo& info)
|
||||
{
|
||||
std::vector<int> values = { 8, 9, 10, 11, 12, 14, 16, 18, 22, 24, 26, 28, 36, 48, 72 };
|
||||
int h = 0;
|
||||
|
||||
// For SpriteSheet fonts we can offer the specific size that matches
|
||||
// the bitmap font (+ x2 + x3)
|
||||
text::FontRef font = Fonts::instance()->fontFromInfo(info);
|
||||
if (font && font->type() == text::FontType::SpriteSheet) {
|
||||
h = int(font->defaultSize());
|
||||
if (h > 0) {
|
||||
for (int i = h; i < h * 4; i += h) {
|
||||
if (!base::contains(values, i))
|
||||
values.insert(std::upper_bound(values.begin(), values.end(), i), i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteAllItems();
|
||||
for (int i : values) {
|
||||
if (h && (i % h) == 0)
|
||||
addItem(fmt::format("{}*", i));
|
||||
else
|
||||
addItem(fmt::format("{}", i));
|
||||
}
|
||||
}
|
||||
|
||||
void FontEntry::FontSize::onEntryChange()
|
||||
|
|
@ -221,31 +251,22 @@ void FontEntry::FontSize::onEntryChange()
|
|||
Change();
|
||||
}
|
||||
|
||||
FontEntry::FontStyle::FontStyle() : ButtonSet(2, true)
|
||||
FontEntry::FontStyle::FontStyle() : ButtonSet(3, true)
|
||||
{
|
||||
addItem("B");
|
||||
addItem("I");
|
||||
addItem("...");
|
||||
setMultiMode(MultiMode::Set);
|
||||
}
|
||||
|
||||
FontEntry::FontLigatures::FontLigatures() : ButtonSet(1, true)
|
||||
{
|
||||
addItem("fi");
|
||||
setMultiMode(MultiMode::Set);
|
||||
}
|
||||
|
||||
FontEntry::FontEntry() : m_antialias("Antialias")
|
||||
FontEntry::FontEntry()
|
||||
{
|
||||
m_face.setExpansive(true);
|
||||
m_size.setExpansive(false);
|
||||
m_style.setExpansive(false);
|
||||
m_ligatures.setExpansive(false);
|
||||
m_antialias.setExpansive(false);
|
||||
addChild(&m_face);
|
||||
addChild(&m_size);
|
||||
addChild(&m_style);
|
||||
addChild(&m_ligatures);
|
||||
addChild(&m_antialias);
|
||||
|
||||
m_face.setMinSize(gfx::Size(128 * guiscale(), 0));
|
||||
|
||||
|
|
@ -253,52 +274,20 @@ FontEntry::FontEntry() : m_antialias("Antialias")
|
|||
if (newTypeName.size() > 0)
|
||||
setInfo(newTypeName, from);
|
||||
else {
|
||||
setInfo(FontInfo(newTypeName, m_info.size(), m_info.style(), m_info.flags()), from);
|
||||
setInfo(
|
||||
FontInfo(newTypeName, m_info.size(), m_info.style(), m_info.flags(), m_info.hinting()),
|
||||
from);
|
||||
}
|
||||
invalidate();
|
||||
});
|
||||
|
||||
m_size.Change.connect([this]() {
|
||||
const float newSize = std::strtof(m_size.getValue().c_str(), nullptr);
|
||||
setInfo(FontInfo(m_info, newSize, m_info.style(), m_info.flags()), From::Size);
|
||||
setInfo(FontInfo(m_info, newSize, m_info.style(), m_info.flags(), m_info.hinting()),
|
||||
From::Size);
|
||||
});
|
||||
|
||||
m_style.ItemChange.connect([this](ButtonSet::Item* item) {
|
||||
text::FontStyle style = m_info.style();
|
||||
switch (m_style.getItemIndex(item)) {
|
||||
// Bold button changed
|
||||
case 0: {
|
||||
const bool bold = m_style.getItem(0)->isSelected();
|
||||
style = text::FontStyle(
|
||||
bold ? text::FontStyle::Weight::Bold : text::FontStyle::Weight::Normal,
|
||||
style.width(),
|
||||
style.slant());
|
||||
break;
|
||||
}
|
||||
// Italic button changed
|
||||
case 1: {
|
||||
const bool italic = m_style.getItem(1)->isSelected();
|
||||
style = text::FontStyle(
|
||||
style.weight(),
|
||||
style.width(),
|
||||
italic ? text::FontStyle::Slant::Italic : text::FontStyle::Slant::Upright);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags()), From::Style);
|
||||
});
|
||||
|
||||
auto flagsChange = [this]() {
|
||||
FontInfo::Flags flags = FontInfo::Flags::None;
|
||||
if (m_antialias.isSelected())
|
||||
flags |= FontInfo::Flags::Antialias;
|
||||
if (m_ligatures.getItem(0)->isSelected())
|
||||
flags |= FontInfo::Flags::Ligatures;
|
||||
setInfo(FontInfo(m_info, m_info.size(), m_info.style(), flags), From::Flags);
|
||||
};
|
||||
m_ligatures.ItemChange.connect(flagsChange);
|
||||
m_antialias.Click.connect(flagsChange);
|
||||
m_style.ItemChange.connect(&FontEntry::onStyleItemClick, this);
|
||||
}
|
||||
|
||||
// Defined here as FontPopup type is not fully defined in the header
|
||||
|
|
@ -314,20 +303,94 @@ void FontEntry::setInfo(const FontInfo& info, const From fromField)
|
|||
if (fromField != From::Face)
|
||||
m_face.setText(info.title());
|
||||
|
||||
if (fromField != From::Size)
|
||||
if (fromField != From::Size) {
|
||||
m_size.updateForFont(info);
|
||||
m_size.setValue(fmt::format("{}", info.size()));
|
||||
}
|
||||
|
||||
if (fromField != From::Style) {
|
||||
m_style.getItem(0)->setSelected(info.style().weight() >= text::FontStyle::Weight::SemiBold);
|
||||
m_style.getItem(1)->setSelected(info.style().slant() != text::FontStyle::Slant::Upright);
|
||||
}
|
||||
|
||||
if (fromField != From::Flags) {
|
||||
m_ligatures.getItem(0)->setSelected(info.ligatures());
|
||||
m_antialias.setSelected(info.antialias());
|
||||
}
|
||||
|
||||
FontChange(m_info, fromField);
|
||||
}
|
||||
|
||||
void FontEntry::onStyleItemClick(ButtonSet::Item* item)
|
||||
{
|
||||
text::FontStyle style = m_info.style();
|
||||
|
||||
switch (m_style.getItemIndex(item)) {
|
||||
// Bold button changed
|
||||
case 0: {
|
||||
const bool bold = m_style.getItem(0)->isSelected();
|
||||
style = text::FontStyle(
|
||||
bold ? text::FontStyle::Weight::Bold : text::FontStyle::Weight::Normal,
|
||||
style.width(),
|
||||
style.slant());
|
||||
|
||||
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
|
||||
From::Style);
|
||||
break;
|
||||
}
|
||||
// Italic button changed
|
||||
case 1: {
|
||||
const bool italic = m_style.getItem(1)->isSelected();
|
||||
style = text::FontStyle(
|
||||
style.weight(),
|
||||
style.width(),
|
||||
italic ? text::FontStyle::Slant::Italic : text::FontStyle::Slant::Upright);
|
||||
|
||||
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
|
||||
From::Style);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
item->setSelected(false); // Unselect the "..." button
|
||||
|
||||
ui::PopupWindow popup;
|
||||
app::gen::FontStyle content;
|
||||
|
||||
content.antialias()->setSelected(m_info.antialias());
|
||||
content.ligatures()->setSelected(m_info.ligatures());
|
||||
content.hinting()->setSelected(m_info.hinting() == text::FontHinting::Normal);
|
||||
|
||||
auto flagsChange = [this, &content]() {
|
||||
FontInfo::Flags flags = FontInfo::Flags::None;
|
||||
if (content.antialias()->isSelected())
|
||||
flags |= FontInfo::Flags::Antialias;
|
||||
if (content.ligatures()->isSelected())
|
||||
flags |= FontInfo::Flags::Ligatures;
|
||||
setInfo(FontInfo(m_info, m_info.size(), m_info.style(), flags, m_info.hinting()),
|
||||
From::Flags);
|
||||
};
|
||||
|
||||
auto hintingChange = [this, &content]() {
|
||||
auto hinting = (content.hinting()->isSelected() ? text::FontHinting::Normal :
|
||||
text::FontHinting::None);
|
||||
|
||||
setInfo(FontInfo(m_info, m_info.size(), m_info.style(), m_info.flags(), hinting),
|
||||
From::Hinting);
|
||||
};
|
||||
|
||||
content.antialias()->Click.connect(flagsChange);
|
||||
content.ligatures()->Click.connect(flagsChange);
|
||||
content.hinting()->Click.connect(hintingChange);
|
||||
|
||||
popup.addChild(&content);
|
||||
popup.remapWindow();
|
||||
|
||||
gfx::Rect rc = item->bounds();
|
||||
rc.y += rc.h - popup.border().bottom();
|
||||
|
||||
ui::fit_bounds(display(), &popup, gfx::Rect(rc.origin(), popup.sizeHint()));
|
||||
|
||||
popup.Open.connect([&popup] { popup.setHotRegion(gfx::Region(popup.boundsOnScreen())); });
|
||||
|
||||
popup.openWindowInForeground();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (c) 2024 Igara Studio S.A.
|
||||
// Copyright (c) 2024-2025 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
#define APP_UI_FONT_ENTRY_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/font_info.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
#include "app/ui/button_set.h"
|
||||
#include "app/ui/search_entry.h"
|
||||
#include "ui/box.h"
|
||||
|
|
@ -28,6 +28,7 @@ public:
|
|||
Size,
|
||||
Style,
|
||||
Flags,
|
||||
Hinting,
|
||||
Popup,
|
||||
};
|
||||
|
||||
|
|
@ -40,6 +41,8 @@ public:
|
|||
obs::signal<void(const FontInfo&, From)> FontChange;
|
||||
|
||||
private:
|
||||
void onStyleItemClick(ButtonSet::Item* item);
|
||||
|
||||
class FontFace : public SearchEntry {
|
||||
public:
|
||||
FontFace();
|
||||
|
|
@ -62,6 +65,7 @@ private:
|
|||
class FontSize : public ui::ComboBox {
|
||||
public:
|
||||
FontSize();
|
||||
void updateForFont(const FontInfo& info);
|
||||
|
||||
protected:
|
||||
void onEntryChange() override;
|
||||
|
|
@ -72,17 +76,10 @@ private:
|
|||
FontStyle();
|
||||
};
|
||||
|
||||
class FontLigatures : public ButtonSet {
|
||||
public:
|
||||
FontLigatures();
|
||||
};
|
||||
|
||||
FontInfo m_info;
|
||||
FontFace m_face;
|
||||
FontSize m_size;
|
||||
FontStyle m_style;
|
||||
FontLigatures m_ligatures;
|
||||
ui::CheckBox m_antialias;
|
||||
bool m_lockFace = false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2020-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -13,13 +13,14 @@
|
|||
|
||||
#include "app/app.h"
|
||||
#include "app/file_selector.h"
|
||||
#include "app/font_info.h"
|
||||
#include "app/font_path.h"
|
||||
#include "app/fonts/font_data.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
#include "app/fonts/font_path.h"
|
||||
#include "app/fonts/fonts.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/match_words.h"
|
||||
#include "app/recent_files.h"
|
||||
#include "app/ui/separator_in_view.h"
|
||||
#include "app/ui/skin/font_data.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "app/util/conversion_to_surface.h"
|
||||
#include "app/util/render_text.h"
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
#include "ui/message.h"
|
||||
#include "ui/paint_event.h"
|
||||
#include "ui/size_hint_event.h"
|
||||
#include "ui/system.h"
|
||||
#include "ui/theme.h"
|
||||
#include "ui/view.h"
|
||||
|
||||
|
|
@ -56,7 +58,18 @@ namespace app {
|
|||
|
||||
using namespace ui;
|
||||
|
||||
static std::map<std::string, os::SurfaceRef> g_thumbnails;
|
||||
namespace {
|
||||
|
||||
struct ThumbnailInfo {
|
||||
os::SurfaceRef surface;
|
||||
float baseline = 0.0f;
|
||||
float descent = 0.0f;
|
||||
float ascent = 0.0f;
|
||||
};
|
||||
|
||||
static std::map<std::string, ThumbnailInfo> g_thumbnails;
|
||||
|
||||
} // namespace
|
||||
|
||||
class FontItem : public ListItem {
|
||||
public:
|
||||
|
|
@ -108,52 +121,96 @@ public:
|
|||
obs::signal<void()> ThumbnailGenerated;
|
||||
|
||||
private:
|
||||
void getCachedThumbnail() { m_thumbnail = g_thumbnails[m_fontInfo.thumbnailId()]; }
|
||||
void getCachedThumbnail()
|
||||
{
|
||||
auto it = g_thumbnails.find(m_fontInfo.thumbnailId());
|
||||
if (it == g_thumbnails.end())
|
||||
return;
|
||||
m_thumbnail = it->second;
|
||||
}
|
||||
|
||||
float onGetTextBaseline() const override
|
||||
{
|
||||
text::FontMetrics metrics;
|
||||
font()->metrics(&metrics);
|
||||
const float descent = std::max<float>(metrics.descent, m_thumbnail.descent);
|
||||
return bounds().h - descent;
|
||||
}
|
||||
|
||||
void onPaint(PaintEvent& ev) override
|
||||
{
|
||||
ListItem::onPaint(ev);
|
||||
|
||||
generateThumbnail();
|
||||
if (!m_thumbnail.surface)
|
||||
return;
|
||||
|
||||
if (m_thumbnail) {
|
||||
const auto* theme = app::skin::SkinTheme::get(this);
|
||||
Graphics* g = ev.graphics();
|
||||
g->drawColoredRgbaSurface(m_thumbnail.get(), theme->colors.text(), textWidth() + 4, 0);
|
||||
}
|
||||
Graphics* g = ev.graphics();
|
||||
const auto* theme = app::skin::SkinTheme::get(this);
|
||||
const float y = textBaseline() - m_thumbnail.baseline;
|
||||
|
||||
g->drawColoredRgbaSurface(m_thumbnail.surface.get(),
|
||||
theme->colors.text(),
|
||||
textWidth() + 4 * guiscale(),
|
||||
y);
|
||||
}
|
||||
|
||||
void onSizeHint(SizeHintEvent& ev) override
|
||||
{
|
||||
ListItem::onSizeHint(ev);
|
||||
if (m_thumbnail) {
|
||||
gfx::Size sz = ev.sizeHint();
|
||||
ev.setSizeHint(sz.w + 4 + m_thumbnail->width(), std::max(sz.h, m_thumbnail->height()));
|
||||
}
|
||||
if (!m_thumbnail.surface)
|
||||
return;
|
||||
|
||||
text::FontMetrics metrics;
|
||||
font()->metrics(&metrics);
|
||||
const float lineHeight = std::max<float>(metrics.descent, m_thumbnail.descent) -
|
||||
std::min<float>(metrics.ascent, m_thumbnail.ascent);
|
||||
|
||||
gfx::Size sz = ev.sizeHint();
|
||||
ev.setSizeHint(sz.w + 4 * guiscale() + m_thumbnail.surface->width(),
|
||||
std::max<float>(sz.h, lineHeight));
|
||||
}
|
||||
|
||||
void generateThumbnail()
|
||||
{
|
||||
if (m_thumbnail)
|
||||
if (m_thumbnail.surface)
|
||||
return;
|
||||
|
||||
const auto* theme = app::skin::SkinTheme::get(this);
|
||||
|
||||
try {
|
||||
Fonts* fonts = Fonts::instance();
|
||||
const FontInfo fontInfoDefSize(m_fontInfo,
|
||||
FontInfo::kDefaultSize,
|
||||
text::FontStyle(),
|
||||
FontInfo::Flags::Antialias);
|
||||
FontInfo::Flags::Antialias,
|
||||
text::FontHinting::Normal);
|
||||
const text::FontRef font = fonts->fontFromInfo(fontInfoDefSize);
|
||||
if (!font)
|
||||
return;
|
||||
|
||||
doc::ImageRef image = render_text(fontInfoDefSize, text(), gfx::rgba(0, 0, 0));
|
||||
if (font->type() != text::FontType::SpriteSheet)
|
||||
font->setSize(12.0f);
|
||||
|
||||
text::TextBlobRef blob = text::TextBlob::MakeWithShaper(fonts->fontMgr(), font, text());
|
||||
if (!blob)
|
||||
return;
|
||||
|
||||
doc::ImageRef image = render_text_blob(blob, gfx::rgba(0, 0, 0));
|
||||
if (!image)
|
||||
return;
|
||||
|
||||
// This font metrics
|
||||
text::FontMetrics metrics;
|
||||
font->metrics(&metrics);
|
||||
m_thumbnail.baseline = blob->baseline();
|
||||
m_thumbnail.descent = metrics.descent;
|
||||
m_thumbnail.ascent = metrics.ascent;
|
||||
|
||||
// Convert the doc::Image into a os::Surface
|
||||
m_thumbnail = os::System::instance()->makeRgbaSurface(image->width(), image->height());
|
||||
m_thumbnail.surface = os::System::instance()->makeRgbaSurface(image->width(),
|
||||
image->height());
|
||||
convert_image_to_surface(image.get(),
|
||||
nullptr,
|
||||
m_thumbnail.get(),
|
||||
m_thumbnail.surface.get(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
|
@ -173,7 +230,7 @@ private:
|
|||
|
||||
void onSelect(bool selected) override
|
||||
{
|
||||
if (!selected || m_thumbnail)
|
||||
if (!selected || m_thumbnail.surface)
|
||||
return;
|
||||
|
||||
ListBox* listbox = static_cast<ListBox*>(parent());
|
||||
|
|
@ -185,7 +242,7 @@ private:
|
|||
}
|
||||
|
||||
private:
|
||||
os::SurfaceRef m_thumbnail;
|
||||
ThumbnailInfo m_thumbnail;
|
||||
FontInfo m_fontInfo;
|
||||
text::FontStyleSetRef m_set;
|
||||
};
|
||||
|
|
@ -242,8 +299,9 @@ FontPopup::FontPopup(const FontInfo& fontInfo)
|
|||
m_listBox.addChild(m_pinnedSeparator);
|
||||
|
||||
// Default fonts
|
||||
Fonts* fonts = Fonts::instance();
|
||||
bool first = true;
|
||||
for (auto kv : skin::SkinTheme::get(this)->getWellKnownFonts()) {
|
||||
for (const auto& kv : fonts->definedFonts()) {
|
||||
if (!kv.second->filename().empty()) {
|
||||
if (first) {
|
||||
m_listBox.addChild(new SeparatorInView(Strings::font_popup_theme_fonts()));
|
||||
|
|
@ -254,75 +312,17 @@ FontPopup::FontPopup(const FontInfo& fontInfo)
|
|||
}
|
||||
|
||||
// Create one FontItem for each font
|
||||
m_listBox.addChild(new SeparatorInView(Strings::font_popup_system_fonts()));
|
||||
bool empty = true;
|
||||
m_systemFontsSeparator = new SeparatorInView(Strings::font_popup_system_fonts());
|
||||
m_listBox.addChild(m_systemFontsSeparator);
|
||||
|
||||
// Get system fonts from laf-text module
|
||||
const text::FontMgrRef fontMgr = theme()->fontMgr();
|
||||
const int n = fontMgr->countFamilies();
|
||||
if (n > 0) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
std::string name = fontMgr->familyName(i);
|
||||
text::FontStyleSetRef set = fontMgr->familyStyleSet(i);
|
||||
if (set && set->count() > 0) {
|
||||
// Match the typeface with the default FontStyle (Normal
|
||||
// weight, Upright slant, etc.)
|
||||
auto typeface = set->matchStyle(text::FontStyle());
|
||||
if (typeface) {
|
||||
auto* item = new FontItem(name, typeface->fontStyle(), set);
|
||||
item->ThumbnailGenerated.connect([this] { onThumbnailGenerated(); });
|
||||
m_listBox.addChild(item);
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get fonts listing .ttf files TODO we should be able to remove
|
||||
// this code in the future (probably after DirectWrite API is always
|
||||
// available).
|
||||
else {
|
||||
base::paths fontDirs;
|
||||
get_font_dirs(fontDirs);
|
||||
|
||||
// Create a list of fullpaths to every font found in all font
|
||||
// directories (fontDirs)
|
||||
base::paths files;
|
||||
for (const auto& fontDir : fontDirs) {
|
||||
for (const auto& file : base::list_files(fontDir, base::ItemType::Files)) {
|
||||
files.push_back(base::join_path(fontDir, file));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all files by "file title"
|
||||
std::sort(files.begin(), files.end(), [](const std::string& a, const std::string& b) {
|
||||
return base::utf8_icmp(base::get_file_title(a), base::get_file_title(b)) < 0;
|
||||
});
|
||||
|
||||
for (auto& file : files) {
|
||||
std::string ext = base::string_to_lower(base::get_file_extension(file));
|
||||
if (ext == "ttf" || ext == "ttc" || ext == "otf" || ext == "dfont") {
|
||||
m_listBox.addChild(new FontItem(file));
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty)
|
||||
m_listBox.addChild(new ListItem(Strings::font_popup_empty_fonts()));
|
||||
|
||||
for (auto* child : m_listBox.children()) {
|
||||
if (auto* childItem = dynamic_cast<FontItem*>(child)) {
|
||||
if (childItem->fontInfo().title() == childItem->text()) {
|
||||
m_listBox.selectChild(childItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_listFontsTask.run([this](base::task_token& token) { listSystemFonts(token); });
|
||||
}
|
||||
|
||||
FontPopup::~FontPopup()
|
||||
{
|
||||
m_timer.stop();
|
||||
m_listFontsTask.cancel();
|
||||
m_listFontsTask.wait();
|
||||
}
|
||||
|
||||
void FontPopup::setSearchText(const std::string& searchText)
|
||||
|
|
@ -428,7 +428,8 @@ void FontPopup::onThumbnailGenerated()
|
|||
void FontPopup::onTickRelayout()
|
||||
{
|
||||
m_popup->view()->updateView();
|
||||
m_timer.stop();
|
||||
if (!m_listFontsTask.running())
|
||||
m_timer.stop();
|
||||
}
|
||||
|
||||
bool FontPopup::onProcessMessage(ui::Message* msg)
|
||||
|
|
@ -449,4 +450,105 @@ bool FontPopup::onProcessMessage(ui::Message* msg)
|
|||
return ui::PopupWindow::onProcessMessage(msg);
|
||||
}
|
||||
|
||||
void FontPopup::listSystemFonts(base::task_token& token)
|
||||
{
|
||||
Fonts* fonts = Fonts::instance();
|
||||
bool empty = true;
|
||||
|
||||
// Get system fonts from laf-text module
|
||||
const text::FontMgrRef fontMgr = fonts->fontMgr();
|
||||
const int n = fontMgr->countFamilies();
|
||||
if (n > 0) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
std::string name = fontMgr->familyName(i);
|
||||
text::FontStyleSetRef set = fontMgr->familyStyleSet(i);
|
||||
if (set && set->count() > 0) {
|
||||
// Match the typeface with the default FontStyle (Normal
|
||||
// weight, Upright slant, etc.)
|
||||
auto typeface = set->matchStyle(text::FontStyle());
|
||||
if (typeface) {
|
||||
ui::execute_from_ui_thread([=, &token] {
|
||||
if (token.canceled())
|
||||
return;
|
||||
|
||||
auto* item = new FontItem(name, typeface->fontStyle(), set);
|
||||
item->ThumbnailGenerated.connect([this] { onThumbnailGenerated(); });
|
||||
|
||||
int j = m_listBox.getChildIndex(m_systemFontsSeparator) + 1;
|
||||
for (; j < m_listBox.getItemsCount(); ++j) {
|
||||
if (name < m_listBox.at(j)->text())
|
||||
break;
|
||||
}
|
||||
m_listBox.insertChild(j, item);
|
||||
layout();
|
||||
});
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (token.canceled())
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
// Get fonts listing .ttf files TODO we should be able to remove
|
||||
// this code in the future (probably after DirectWrite API is always
|
||||
// available).
|
||||
else {
|
||||
base::paths fontDirs;
|
||||
get_font_dirs(fontDirs);
|
||||
|
||||
// Create a list of fullpaths to every font found in all font
|
||||
// directories (fontDirs)
|
||||
base::paths files;
|
||||
for (const auto& fontDir : fontDirs) {
|
||||
for (const auto& file : base::list_files(fontDir, base::ItemType::Files)) {
|
||||
files.push_back(base::join_path(fontDir, file));
|
||||
if (token.canceled())
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all files by "file title"
|
||||
std::sort(files.begin(), files.end(), [](const std::string& a, const std::string& b) {
|
||||
return base::utf8_icmp(base::get_file_title(a), base::get_file_title(b)) < 0;
|
||||
});
|
||||
|
||||
for (auto& file : files) {
|
||||
std::string ext = base::string_to_lower(base::get_file_extension(file));
|
||||
if (ext == "ttf" || ext == "ttc" || ext == "otf" || ext == "dfont") {
|
||||
ui::execute_from_ui_thread([this, file, &token] {
|
||||
if (token.canceled())
|
||||
return;
|
||||
|
||||
m_listBox.addChild(new FontItem(file));
|
||||
});
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done:;
|
||||
if (token.canceled())
|
||||
return;
|
||||
|
||||
if (empty) {
|
||||
ui::execute_from_ui_thread([this, &token] {
|
||||
if (token.canceled())
|
||||
return;
|
||||
|
||||
m_listBox.addChild(new ListItem(Strings::font_popup_empty_fonts()));
|
||||
layout();
|
||||
});
|
||||
}
|
||||
|
||||
ui::execute_from_ui_thread([this, &token] {
|
||||
if (token.canceled())
|
||||
return;
|
||||
|
||||
// Stop the view relayout
|
||||
onTickRelayout();
|
||||
m_timer.stop();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2021-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2021-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
#define APP_UI_FONT_POPUP_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/task.h"
|
||||
#include "ui/listbox.h"
|
||||
#include "ui/popup_window.h"
|
||||
#include "ui/timer.h"
|
||||
|
|
@ -57,10 +58,14 @@ protected:
|
|||
bool onProcessMessage(ui::Message* msg) override;
|
||||
|
||||
private:
|
||||
void listSystemFonts(base::task_token& token);
|
||||
|
||||
gen::FontPopup* m_popup;
|
||||
Widget* m_systemFontsSeparator;
|
||||
FontListBox m_listBox;
|
||||
ui::Timer m_timer;
|
||||
ui::Widget* m_pinnedSeparator = nullptr;
|
||||
app::Task m_listFontsTask;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ IconButton::IconButton(const SkinPartPtr& part) : Button(""), m_part(part)
|
|||
initTheme();
|
||||
}
|
||||
|
||||
void IconButton::setIcon(const skin::SkinPartPtr& part)
|
||||
{
|
||||
m_part = part;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void IconButton::onInitTheme(InitThemeEvent& ev)
|
||||
{
|
||||
Button::onInitTheme(ev);
|
||||
|
|
@ -44,7 +50,7 @@ void IconButton::onSizeHint(SizeHintEvent& ev)
|
|||
|
||||
void IconButton::onPaint(PaintEvent& ev)
|
||||
{
|
||||
auto theme = SkinTheme::get(this);
|
||||
const auto* theme = SkinTheme::get(this);
|
||||
Graphics* g = ev.graphics();
|
||||
gfx::Color fg, bg;
|
||||
|
||||
|
|
@ -61,9 +67,11 @@ void IconButton::onPaint(PaintEvent& ev)
|
|||
bg = bgColor();
|
||||
}
|
||||
|
||||
g->fillRect(bg, g->getClipBounds());
|
||||
if (!isTransparent()) {
|
||||
g->fillRect(bg, g->getClipBounds());
|
||||
}
|
||||
|
||||
gfx::Rect bounds = clientBounds();
|
||||
const gfx::Rect bounds = clientBounds();
|
||||
os::Surface* icon = m_part->bitmap(0);
|
||||
g->drawColoredRgbaSurface(icon,
|
||||
fg,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ namespace app {
|
|||
|
||||
class IconButton : public ui::Button {
|
||||
public:
|
||||
IconButton(const skin::SkinPartPtr& part);
|
||||
explicit IconButton(const skin::SkinPartPtr& part);
|
||||
void setIcon(const skin::SkinPartPtr& part);
|
||||
|
||||
protected:
|
||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
#include "app/ui/key_context.h"
|
||||
#include "base/convert_to.h"
|
||||
#include "base/vector2d.h"
|
||||
#include "ui/accelerator.h"
|
||||
#include "ui/shortcut.h"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
|
@ -106,7 +106,7 @@ inline KeyAction operator&(KeyAction a, KeyAction b)
|
|||
class Key;
|
||||
using KeyPtr = std::shared_ptr<Key>;
|
||||
using Keys = std::vector<KeyPtr>;
|
||||
using KeySourceAccelList = std::vector<std::pair<KeySource, ui::Accelerator>>;
|
||||
using KeySourceShortcutList = std::vector<std::pair<KeySource, ui::Shortcut>>;
|
||||
using DragVector = base::Vector2d<double>;
|
||||
|
||||
class Key {
|
||||
|
|
@ -119,29 +119,26 @@ public:
|
|||
static KeyPtr MakeDragAction(WheelAction dragAction);
|
||||
|
||||
KeyType type() const { return m_type; }
|
||||
const ui::Accelerators& accels() const;
|
||||
const KeySourceAccelList addsKeys() const { return m_adds; }
|
||||
const KeySourceAccelList delsKeys() const { return m_dels; }
|
||||
const ui::Shortcuts& shortcuts() const;
|
||||
const KeySourceShortcutList& addsKeys() const { return m_adds; }
|
||||
const KeySourceShortcutList& delsKeys() const { return m_dels; }
|
||||
|
||||
void add(const ui::Accelerator& accel, const KeySource source, KeyboardShortcuts& globalKeys);
|
||||
const ui::Accelerator* isPressed(const ui::Message* msg,
|
||||
const KeyboardShortcuts& globalKeys,
|
||||
const KeyContext keyContext) const;
|
||||
const ui::Accelerator* isPressed(const ui::Message* msg,
|
||||
const KeyboardShortcuts& globalKeys) const;
|
||||
void add(const ui::Shortcut& shortcut, KeySource source, KeyboardShortcuts& globalKeys);
|
||||
const ui::Shortcut* isPressed(const ui::Message* msg, KeyContext keyContext) const;
|
||||
const ui::Shortcut* isPressed(const ui::Message* msg) const;
|
||||
bool isPressed() const;
|
||||
bool isLooselyPressed() const;
|
||||
bool isCommandListed() const;
|
||||
|
||||
bool hasAccel(const ui::Accelerator& accel) const;
|
||||
bool hasUserDefinedAccels() const;
|
||||
bool hasShortcut(const ui::Shortcut& shortcut) const;
|
||||
bool hasUserDefinedShortcuts() const;
|
||||
|
||||
// The KeySource indicates from where the key was disabled
|
||||
// (e.g. if it was removed from an extension-defined file, or from
|
||||
// user-defined).
|
||||
void disableAccel(const ui::Accelerator& accel, const KeySource source);
|
||||
void disableShortcut(const ui::Shortcut& shortcut, KeySource source);
|
||||
|
||||
// Resets user accelerators to the original & extension-defined ones.
|
||||
// Resets user shortcuts to the original & extension-defined ones.
|
||||
void reset();
|
||||
|
||||
void copyOriginalToUser();
|
||||
|
|
@ -164,11 +161,11 @@ public:
|
|||
|
||||
private:
|
||||
KeyType m_type;
|
||||
KeySourceAccelList m_adds;
|
||||
KeySourceAccelList m_dels;
|
||||
// Final list of accelerators after processing the
|
||||
KeySourceShortcutList m_adds;
|
||||
KeySourceShortcutList m_dels;
|
||||
// Final list of shortcuts after processing the
|
||||
// addition/deletion of extension-defined & user-defined keys.
|
||||
mutable std::unique_ptr<ui::Accelerators> m_accels;
|
||||
mutable std::unique_ptr<ui::Shortcuts> m_shortcuts;
|
||||
KeyContext m_keycontext;
|
||||
|
||||
// for KeyType::Command
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
|
@ -28,8 +28,8 @@
|
|||
#include "app/xml_document.h"
|
||||
#include "app/xml_exception.h"
|
||||
#include "fmt/format.h"
|
||||
#include "ui/accelerator.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/shortcut.h"
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
|
|
@ -244,13 +244,13 @@ std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelActio
|
|||
return std::string();
|
||||
}
|
||||
|
||||
void erase_accel(app::KeySourceAccelList& kvs,
|
||||
const app::KeySource source,
|
||||
const ui::Accelerator& accel)
|
||||
void erase_shortcut(app::KeySourceShortcutList& kvs,
|
||||
const app::KeySource source,
|
||||
const ui::Shortcut& shortcut)
|
||||
{
|
||||
for (auto it = kvs.begin(); it != kvs.end();) {
|
||||
auto& kv = *it;
|
||||
if (kv.first == source && kv.second == accel) {
|
||||
if (kv.first == source && kv.second == shortcut) {
|
||||
it = kvs.erase(it);
|
||||
}
|
||||
else
|
||||
|
|
@ -258,7 +258,7 @@ void erase_accel(app::KeySourceAccelList& kvs,
|
|||
}
|
||||
}
|
||||
|
||||
void erase_accels(app::KeySourceAccelList& kvs, const app::KeySource source)
|
||||
void erase_shortcuts(app::KeySourceShortcutList& kvs, const app::KeySource source)
|
||||
{
|
||||
for (auto it = kvs.begin(); it != kvs.end();) {
|
||||
auto& kv = *it;
|
||||
|
|
@ -436,104 +436,100 @@ KeyPtr Key::MakeDragAction(WheelAction dragAction)
|
|||
return k;
|
||||
}
|
||||
|
||||
const ui::Accelerators& Key::accels() const
|
||||
const ui::Shortcuts& Key::shortcuts() const
|
||||
{
|
||||
if (!m_accels) {
|
||||
m_accels = std::make_unique<ui::Accelerators>();
|
||||
if (!m_shortcuts) {
|
||||
m_shortcuts = std::make_unique<ui::Shortcuts>();
|
||||
|
||||
// Add default keys
|
||||
for (const auto& kv : m_adds) {
|
||||
if (kv.first == KeySource::Original)
|
||||
m_accels->add(kv.second);
|
||||
m_shortcuts->add(kv.second);
|
||||
}
|
||||
|
||||
// Delete/add extension-defined keys
|
||||
for (const auto& kv : m_dels) {
|
||||
if (kv.first == KeySource::ExtensionDefined)
|
||||
m_accels->remove(kv.second);
|
||||
m_shortcuts->remove(kv.second);
|
||||
else {
|
||||
ASSERT(kv.first != KeySource::Original);
|
||||
}
|
||||
}
|
||||
for (const auto& kv : m_adds) {
|
||||
if (kv.first == KeySource::ExtensionDefined)
|
||||
m_accels->add(kv.second);
|
||||
m_shortcuts->add(kv.second);
|
||||
}
|
||||
|
||||
// Delete/add user-defined keys
|
||||
for (const auto& kv : m_dels) {
|
||||
if (kv.first == KeySource::UserDefined)
|
||||
m_accels->remove(kv.second);
|
||||
m_shortcuts->remove(kv.second);
|
||||
}
|
||||
for (const auto& kv : m_adds) {
|
||||
if (kv.first == KeySource::UserDefined)
|
||||
m_accels->add(kv.second);
|
||||
m_shortcuts->add(kv.second);
|
||||
}
|
||||
}
|
||||
return *m_accels;
|
||||
return *m_shortcuts;
|
||||
}
|
||||
|
||||
void Key::add(const ui::Accelerator& accel, const KeySource source, KeyboardShortcuts& globalKeys)
|
||||
void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShortcuts& globalKeys)
|
||||
{
|
||||
m_adds.emplace_back(source, accel);
|
||||
m_accels.reset();
|
||||
m_adds.emplace_back(source, shortcut);
|
||||
m_shortcuts.reset();
|
||||
|
||||
// Remove the accelerator from other commands
|
||||
// Remove the shortcut from other commands
|
||||
if (source == KeySource::ExtensionDefined || source == KeySource::UserDefined) {
|
||||
erase_accel(m_dels, source, accel);
|
||||
erase_shortcut(m_dels, source, shortcut);
|
||||
|
||||
globalKeys.disableAccel(accel, source, m_keycontext, this);
|
||||
globalKeys.disableShortcut(shortcut, source, m_keycontext, this);
|
||||
}
|
||||
}
|
||||
|
||||
const ui::Accelerator* Key::isPressed(const Message* msg,
|
||||
const KeyboardShortcuts& globalKeys,
|
||||
const KeyContext keyContext) const
|
||||
const ui::Shortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
|
||||
{
|
||||
if (auto keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
|
||||
for (const Accelerator& accel : accels()) {
|
||||
if (accel.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
|
||||
if (const auto* keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
|
||||
for (const Shortcut& shortcut : shortcuts()) {
|
||||
if (shortcut.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
|
||||
(m_keycontext == KeyContext::Any || m_keycontext == keyContext)) {
|
||||
return &accel;
|
||||
return &shortcut;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
|
||||
for (const Accelerator& accel : accels()) {
|
||||
if ((accel.modifiers() == mouseMsg->modifiers()) &&
|
||||
else if (const auto* mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
|
||||
for (const Shortcut& shortcut : shortcuts()) {
|
||||
if ((shortcut.modifiers() == mouseMsg->modifiers()) &&
|
||||
(m_keycontext == KeyContext::Any ||
|
||||
// TODO we could have multiple mouse wheel key-context,
|
||||
// like "sprite editor" context, or "timeline" context,
|
||||
// etc.
|
||||
m_keycontext == KeyContext::MouseWheel)) {
|
||||
return &accel;
|
||||
return &shortcut;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ui::Accelerator* Key::isPressed(const Message* msg, const KeyboardShortcuts& globalKeys) const
|
||||
const ui::Shortcut* Key::isPressed(const Message* msg) const
|
||||
{
|
||||
return isPressed(msg, globalKeys, globalKeys.getCurrentKeyContext());
|
||||
return isPressed(msg, KeyboardShortcuts::getCurrentKeyContext());
|
||||
}
|
||||
|
||||
bool Key::isPressed() const
|
||||
{
|
||||
for (const Accelerator& accel : this->accels()) {
|
||||
if (accel.isPressed())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const auto& ss = this->shortcuts();
|
||||
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
|
||||
return shortcut.isPressed();
|
||||
});
|
||||
}
|
||||
|
||||
bool Key::isLooselyPressed() const
|
||||
{
|
||||
for (const Accelerator& accel : this->accels()) {
|
||||
if (accel.isLooselyPressed())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const auto& ss = this->shortcuts();
|
||||
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
|
||||
return shortcut.isLooselyPressed();
|
||||
});
|
||||
}
|
||||
|
||||
bool Key::isCommandListed() const
|
||||
|
|
@ -541,51 +537,49 @@ bool Key::isCommandListed() const
|
|||
return type() == KeyType::Command && command()->isListed(params());
|
||||
}
|
||||
|
||||
bool Key::hasAccel(const ui::Accelerator& accel) const
|
||||
bool Key::hasShortcut(const ui::Shortcut& shortcut) const
|
||||
{
|
||||
return accels().has(accel);
|
||||
return shortcuts().has(shortcut);
|
||||
}
|
||||
|
||||
bool Key::hasUserDefinedAccels() const
|
||||
bool Key::hasUserDefinedShortcuts() const
|
||||
{
|
||||
for (const auto& kv : m_adds) {
|
||||
if (kv.first == KeySource::UserDefined)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return std::any_of(m_adds.begin(), m_adds.end(), [](const auto& kv) {
|
||||
return (kv.first == KeySource::UserDefined);
|
||||
});
|
||||
}
|
||||
|
||||
void Key::disableAccel(const ui::Accelerator& accel, const KeySource source)
|
||||
void Key::disableShortcut(const ui::Shortcut& shortcut, const KeySource source)
|
||||
{
|
||||
// It doesn't make sense that the default keyboard shortcuts file
|
||||
// (gui.xml) removes some accelerator.
|
||||
// (gui.xml) removes some shortcut.
|
||||
ASSERT(source != KeySource::Original);
|
||||
|
||||
erase_accel(m_adds, source, accel);
|
||||
erase_accel(m_dels, source, accel);
|
||||
erase_shortcut(m_adds, source, shortcut);
|
||||
erase_shortcut(m_dels, source, shortcut);
|
||||
|
||||
m_dels.emplace_back(source, accel);
|
||||
m_accels.reset();
|
||||
m_dels.emplace_back(source, shortcut);
|
||||
m_shortcuts.reset();
|
||||
}
|
||||
|
||||
void Key::reset()
|
||||
{
|
||||
erase_accels(m_adds, KeySource::UserDefined);
|
||||
erase_accels(m_dels, KeySource::UserDefined);
|
||||
m_accels.reset();
|
||||
erase_shortcuts(m_adds, KeySource::UserDefined);
|
||||
erase_shortcuts(m_dels, KeySource::UserDefined);
|
||||
m_shortcuts.reset();
|
||||
}
|
||||
|
||||
void Key::copyOriginalToUser()
|
||||
{
|
||||
// Erase all user-defined keys
|
||||
erase_accels(m_adds, KeySource::UserDefined);
|
||||
erase_accels(m_dels, KeySource::UserDefined);
|
||||
erase_shortcuts(m_adds, KeySource::UserDefined);
|
||||
erase_shortcuts(m_dels, KeySource::UserDefined);
|
||||
|
||||
// Then copy all original & extension-defined keys as user-defined
|
||||
auto copy = m_adds;
|
||||
for (const auto& kv : copy)
|
||||
m_adds.emplace_back(KeySource::UserDefined, kv.second);
|
||||
m_accels.reset();
|
||||
m_shortcuts.reset();
|
||||
}
|
||||
|
||||
std::string Key::triggerString() const
|
||||
|
|
@ -693,21 +687,21 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
|
|||
// add the keyboard shortcut to the command
|
||||
KeyPtr key = this->command(command_name, params, keycontext);
|
||||
if (key && command_key) {
|
||||
Accelerator accel(command_key);
|
||||
Shortcut shortcut(command_key);
|
||||
|
||||
if (!removed) {
|
||||
key->add(accel, source, *this);
|
||||
key->add(shortcut, source, *this);
|
||||
|
||||
// Add the shortcut to the menuitems with this command
|
||||
// (this is only visual, the
|
||||
// "CustomizedGuiManager::onProcessMessage" is the only
|
||||
// one that process keyboard shortcuts)
|
||||
if (key->accels().size() == 1) {
|
||||
if (key->shortcuts().size() == 1) {
|
||||
AppMenus::instance()->applyShortcutToMenuitemsWithCommand(command, params, key);
|
||||
}
|
||||
}
|
||||
else
|
||||
key->disableAccel(accel, source);
|
||||
key->disableShortcut(shortcut, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -729,12 +723,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
|
|||
KeyPtr key = this->tool(tool);
|
||||
if (key && tool_key) {
|
||||
LOG(VERBOSE, "KEYS: Shortcut for tool %s: %s\n", tool_id, tool_key);
|
||||
Accelerator accel(tool_key);
|
||||
Shortcut shortcut(tool_key);
|
||||
|
||||
if (!removed)
|
||||
key->add(accel, source, *this);
|
||||
key->add(shortcut, source, *this);
|
||||
else
|
||||
key->disableAccel(accel, source);
|
||||
key->disableShortcut(shortcut, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -755,12 +749,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
|
|||
KeyPtr key = this->quicktool(tool);
|
||||
if (key && tool_key) {
|
||||
LOG(VERBOSE, "KEYS: Shortcut for quicktool %s: %s\n", tool_id, tool_key);
|
||||
Accelerator accel(tool_key);
|
||||
Shortcut shortcut(tool_key);
|
||||
|
||||
if (!removed)
|
||||
key->add(accel, source, *this);
|
||||
key->add(shortcut, source, *this);
|
||||
else
|
||||
key->disableAccel(accel, source);
|
||||
key->disableShortcut(shortcut, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -791,12 +785,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
|
|||
action_id,
|
||||
(keycontextstr ? keycontextstr : "Any"),
|
||||
action_key);
|
||||
Accelerator accel(action_key);
|
||||
Shortcut shortcut(action_key);
|
||||
|
||||
if (!removed)
|
||||
key->add(accel, source, *this);
|
||||
key->add(shortcut, source, *this);
|
||||
else
|
||||
key->disableAccel(accel, source);
|
||||
key->disableShortcut(shortcut, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -817,12 +811,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
|
|||
KeyPtr key = this->wheelAction(action);
|
||||
if (key && action_key) {
|
||||
LOG(VERBOSE, "KEYS: Shortcut for wheel action %s: %s\n", action_id, action_key);
|
||||
Accelerator accel(action_key);
|
||||
Shortcut shortcut(action_key);
|
||||
|
||||
if (!removed)
|
||||
key->add(accel, source, *this);
|
||||
key->add(shortcut, source, *this);
|
||||
else
|
||||
key->disableAccel(accel, source);
|
||||
key->disableShortcut(shortcut, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -854,12 +848,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
|
|||
}
|
||||
|
||||
LOG(VERBOSE, "KEYS: Shortcut for drag action %s: %s\n", action_id, action_key);
|
||||
Accelerator accel(action_key);
|
||||
Shortcut shortcut(action_key);
|
||||
|
||||
if (!removed)
|
||||
key->add(accel, source, *this);
|
||||
key->add(shortcut, source, *this);
|
||||
else
|
||||
key->disableAccel(accel, source);
|
||||
key->disableShortcut(shortcut, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -904,24 +898,25 @@ void KeyboardShortcuts::exportFile(const std::string& filename)
|
|||
void KeyboardShortcuts::exportKeys(XMLElement* parent, KeyType type)
|
||||
{
|
||||
for (KeyPtr& key : m_keys) {
|
||||
// Save only user defined accelerators.
|
||||
// Save only user defined shortcuts.
|
||||
if (key->type() != type)
|
||||
continue;
|
||||
|
||||
for (const auto& kv : key->delsKeys())
|
||||
if (kv.first == KeySource::UserDefined)
|
||||
exportAccel(parent, key.get(), kv.second, true);
|
||||
exportShortcut(parent, key.get(), kv.second, true);
|
||||
|
||||
for (const auto& kv : key->addsKeys())
|
||||
if (kv.first == KeySource::UserDefined)
|
||||
exportAccel(parent, key.get(), kv.second, false);
|
||||
exportShortcut(parent, key.get(), kv.second, false);
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardShortcuts::exportAccel(XMLElement* parent,
|
||||
const Key* key,
|
||||
const ui::Accelerator& accel,
|
||||
bool removed)
|
||||
// static
|
||||
void KeyboardShortcuts::exportShortcut(XMLElement* parent,
|
||||
const Key* key,
|
||||
const ui::Shortcut& shortcut,
|
||||
bool removed)
|
||||
{
|
||||
XMLElement* elem = parent->InsertNewChildElement("key");
|
||||
|
||||
|
|
@ -964,7 +959,7 @@ void KeyboardShortcuts::exportAccel(XMLElement* parent,
|
|||
break;
|
||||
}
|
||||
|
||||
elem->SetAttribute("shortcut", accel.toString().c_str());
|
||||
elem->SetAttribute("shortcut", shortcut.toString().c_str());
|
||||
|
||||
if (removed)
|
||||
elem->SetAttribute("removed", "true");
|
||||
|
|
@ -1062,27 +1057,28 @@ KeyPtr KeyboardShortcuts::dragAction(const WheelAction dragAction) const
|
|||
return key;
|
||||
}
|
||||
|
||||
void KeyboardShortcuts::disableAccel(const ui::Accelerator& accel,
|
||||
const KeySource source,
|
||||
const KeyContext keyContext,
|
||||
const Key* newKey)
|
||||
void KeyboardShortcuts::disableShortcut(const ui::Shortcut& shortcut,
|
||||
const KeySource source,
|
||||
const KeyContext keyContext,
|
||||
const Key* newKey)
|
||||
{
|
||||
for (KeyPtr& key : m_keys) {
|
||||
if (key.get() != newKey && key->keycontext() == keyContext && key->hasAccel(accel) &&
|
||||
if (key.get() != newKey && key->keycontext() == keyContext && key->hasShortcut(shortcut) &&
|
||||
// Tools can contain the same keyboard shortcut
|
||||
(key->type() != KeyType::Tool || newKey == nullptr || newKey->type() != KeyType::Tool) &&
|
||||
// DragActions can share the same keyboard shortcut (e.g. to
|
||||
// change different values using different DragVectors)
|
||||
(key->type() != KeyType::DragAction || newKey == nullptr ||
|
||||
newKey->type() != KeyType::DragAction)) {
|
||||
key->disableAccel(accel, source);
|
||||
key->disableShortcut(shortcut, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeyContext KeyboardShortcuts::getCurrentKeyContext() const
|
||||
// static
|
||||
KeyContext KeyboardShortcuts::getCurrentKeyContext()
|
||||
{
|
||||
auto ctx = UIContext::instance();
|
||||
auto* ctx = UIContext::instance();
|
||||
Doc* doc = ctx->activeDocument();
|
||||
if (doc && doc->isMaskVisible() &&
|
||||
// The active key context will be the selectedTool() (in the
|
||||
|
|
@ -1116,7 +1112,7 @@ bool KeyboardShortcuts::getCommandFromKeyMessage(const Message* msg,
|
|||
int n = (contexts[0] != contexts[1] ? 2 : 1);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
for (KeyPtr& key : m_keys) {
|
||||
if (key->type() == KeyType::Command && key->isPressed(msg, *this, contexts[i])) {
|
||||
if (key->type() == KeyType::Command && key->isPressed(msg, contexts[i])) {
|
||||
if (command)
|
||||
*command = key->command();
|
||||
if (params)
|
||||
|
|
@ -1168,12 +1164,12 @@ WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext c
|
|||
const ui::Message* msg)
|
||||
{
|
||||
WheelAction wheelAction = WheelAction::None;
|
||||
const ui::Accelerator* bestAccel = nullptr;
|
||||
const ui::Shortcut* bestShortcut = nullptr;
|
||||
for (const KeyPtr& key : m_keys) {
|
||||
if (key->type() == KeyType::WheelAction && key->keycontext() == context) {
|
||||
const ui::Accelerator* accel = key->isPressed(msg, *this);
|
||||
if ((accel) && (!bestAccel || bestAccel->modifiers() < accel->modifiers())) {
|
||||
bestAccel = accel;
|
||||
const ui::Shortcut* shortcut = key->isPressed(msg);
|
||||
if ((shortcut) && (!bestShortcut || bestShortcut->modifiers() < shortcut->modifiers())) {
|
||||
bestShortcut = shortcut;
|
||||
wheelAction = key->wheelAction();
|
||||
}
|
||||
}
|
||||
|
|
@ -1181,15 +1177,14 @@ WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext c
|
|||
return wheelAction;
|
||||
}
|
||||
|
||||
Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const KeyContext context,
|
||||
const ui::Message* msg)
|
||||
Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const ui::Message* msg)
|
||||
{
|
||||
KeyPtr bestKey = nullptr;
|
||||
Keys keys;
|
||||
for (const KeyPtr& key : m_keys) {
|
||||
if (key->type() == KeyType::DragAction) {
|
||||
const ui::Accelerator* accel = key->isPressed(msg, *this);
|
||||
if (accel) {
|
||||
const ui::Shortcut* shortcut = key->isPressed(msg);
|
||||
if (shortcut) {
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
|
|
@ -1199,11 +1194,9 @@ Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const KeyContext context,
|
|||
|
||||
bool KeyboardShortcuts::hasMouseWheelCustomization() const
|
||||
{
|
||||
for (const KeyPtr& key : m_keys) {
|
||||
if (key->type() == KeyType::WheelAction && key->hasUserDefinedAccels())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return std::any_of(m_keys.begin(), m_keys.end(), [](const KeyPtr& key) {
|
||||
return (key->type() == KeyType::WheelAction && key->hasUserDefinedShortcuts());
|
||||
});
|
||||
}
|
||||
|
||||
void KeyboardShortcuts::clearMouseWheelKeys()
|
||||
|
|
@ -1245,38 +1238,38 @@ void KeyboardShortcuts::setDefaultMouseWheelKeys(const bool zoomWithWheel)
|
|||
|
||||
KeyPtr key;
|
||||
key = std::make_shared<Key>(WheelAction::Zoom);
|
||||
key->add(Accelerator(zoomWithWheel ? kKeyNoneModifier : kKeyCtrlModifier, kKeyNil, 0),
|
||||
key->add(Shortcut(zoomWithWheel ? kKeyNoneModifier : kKeyCtrlModifier, kKeyNil, 0),
|
||||
KeySource::Original,
|
||||
*this);
|
||||
m_keys.push_back(key);
|
||||
|
||||
if (!zoomWithWheel) {
|
||||
key = std::make_shared<Key>(WheelAction::VScroll);
|
||||
key->add(Accelerator(kKeyNoneModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
key->add(Shortcut(kKeyNoneModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
m_keys.push_back(key);
|
||||
}
|
||||
|
||||
key = std::make_shared<Key>(WheelAction::HScroll);
|
||||
key->add(Accelerator(kKeyShiftModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
key->add(Shortcut(kKeyShiftModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
m_keys.push_back(key);
|
||||
|
||||
key = std::make_shared<Key>(WheelAction::FgColor);
|
||||
key->add(Accelerator(kKeyAltModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
key->add(Shortcut(kKeyAltModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
m_keys.push_back(key);
|
||||
|
||||
key = std::make_shared<Key>(WheelAction::BgColor);
|
||||
key->add(Accelerator((KeyModifiers)(kKeyAltModifier | kKeyShiftModifier), kKeyNil, 0),
|
||||
key->add(Shortcut((KeyModifiers)(kKeyAltModifier | kKeyShiftModifier), kKeyNil, 0),
|
||||
KeySource::Original,
|
||||
*this);
|
||||
m_keys.push_back(key);
|
||||
|
||||
if (zoomWithWheel) {
|
||||
key = std::make_shared<Key>(WheelAction::BrushSize);
|
||||
key->add(Accelerator(kKeyCtrlModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
key->add(Shortcut(kKeyCtrlModifier, kKeyNil, 0), KeySource::Original, *this);
|
||||
m_keys.push_back(key);
|
||||
|
||||
key = std::make_shared<Key>(WheelAction::Frame);
|
||||
key->add(Accelerator((KeyModifiers)(kKeyCtrlModifier | kKeyShiftModifier), kKeyNil, 0),
|
||||
key->add(Shortcut((KeyModifiers)(kKeyCtrlModifier | kKeyShiftModifier), kKeyNil, 0),
|
||||
KeySource::Original,
|
||||
*this);
|
||||
m_keys.push_back(key);
|
||||
|
|
@ -1321,9 +1314,9 @@ std::string key_tooltip(const char* str, const app::Key* key)
|
|||
std::string res;
|
||||
if (str)
|
||||
res += str;
|
||||
if (key && !key->accels().empty()) {
|
||||
if (key && !key->shortcuts().empty()) {
|
||||
res += " (";
|
||||
res += key->accels().front().toString();
|
||||
res += key->shortcuts().front().toString();
|
||||
res += ")";
|
||||
}
|
||||
return res;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue