Compare commits

...

85 Commits

Author SHA1 Message Date
David Capello 2b522e222b Rename ui::Accelerator to ui::Shortcut
This was a pending refactor, where "user.aseprite-keys" files were
already using the "shortcut" attribute (instead of "accelerator").
This will include a refactor in the Weblate projects/all translations
to change [select_accelerator] section to [select_shortcut]. But that
must be coordinated after this commit is merged.
2025-06-09 17:20:16 -03:00
Maplegecko 698d79b049 [lua] Make Dialog:separator more in line with other widgets (fix #4989) 2025-06-09 13:26:39 -03:00
David Capello 03422e7251 [lua] Update scripting API version to 33 2025-06-07 12:08:00 -03:00
David Capello 3f1f131a39 Update laf 2025-06-07 12:05:56 -03:00
Christian Kaiser 54ea61fe58 Re-add os.rename and os.remove support 2025-06-07 12:05:34 -03:00
JoshuaL03 4c31c950c5
Fix file dialog size not being loaded properly (fix #4460) (#5202) 2025-06-07 11:30:27 -03:00
martincapello afbd28b3b4 Improve panel resizing performance
By avoiding calculating bounds for HIDDEN panel's children.
2025-06-05 11:11:20 -03:00
Joshua Lopez 1519589184 Fix user data visibility after window size reset (fix aseprite#4923) 2025-06-04 13:37:38 -03:00
Luke Barcenas 2c917e30c0 Fix: Prevent scrollbar from shifting on click 2025-06-03 16:54:42 -03:00
Gaspar Capello 5d0214a89d Fix wrong export of tileset (fix #5053)
Before this fix, when a sprite's color mode was set to Grayscale and
a tileset was exported, the resulting sheet was full of broken
tile images due to a misinterpretation of the pixel format in
the samples.

An unnecessary conversion of the original sprite was also observed
during the "Export Sprite Sheet" command. Specifically,
in the DocExporter::renderTexture function (fix #5088).
2025-06-03 13:19:56 -03:00
David Capello 4b1d49f5dc [ci][clang-tidy] Update clang-tidy-review version
We require the following fix: https://github.com/ZedThree/clang-tidy-review/issues/144
2025-06-02 22:24:42 -03:00
David Capello 0ccc800819 [clang-tidy] Allow if (intCondition) { } to ask for != 0 2025-06-02 20:44:17 -03:00
David Capello 14c0baa3db Convert standalone functions into snake_case 2025-06-02 19:50:05 -03:00
Martín Capello 30dcac99c6 Update copyright's year 2025-06-02 19:45:37 -03:00
Martín Capello d209971f07 Improve feedback when dropping into a cel
Show indicator to let the user know between which layers will the
dropped stuff be inserted
2025-06-02 19:45:37 -03:00
Martín Capello 0a88b86c99 Improve wording and linkage of some functions 2025-06-02 19:45:37 -03:00
Martín Capello ab449a2978 [win] Fix compilation when not using skia backend 2025-06-02 19:45:37 -03:00
Martín Capello b32cd0ff47 Avoid creating surface to determine color mode 2025-06-02 19:45:37 -03:00
Martín Capello ce0b9a6405 Fix tileset addition into destination sprite
Now the tileset addition is made using a command, which allows it to be
undone/redone properly
2025-06-02 19:45:37 -03:00
Martín Capello 67656c4977 Fix crash dropping elements at edge of bottom cel 2025-06-02 19:45:37 -03:00
Martín Capello 7817e7b37a Fix frame displacement of dropped layers 2025-06-02 19:45:37 -03:00
Martín Capello 45fbeda95b Avoid enqueuing events directly to laf-os 2025-06-02 19:45:37 -03:00
Martín Capello a3236bc1e9 Remove ui::Manager as friend of ui::Widget 2025-06-02 19:45:37 -03:00
Martín Capello d37e0df18f Replace use of reset(new...) by std::make_unique() 2025-06-02 19:45:37 -03:00
Martín Capello 7103829e60 Refactor DropOnTimeline command 2025-06-02 19:45:37 -03:00
Martín Capello eab8df6854 Support duplicating layers from other documents
Update DocApi::duplicateLayerAfter and DocApi::duplicateLayerBefore to
allow duplicating layers from documents that are not the same as the
source layer's document
2025-06-02 19:45:37 -03:00
Martín Capello 99a407f0c5 Move OpenFileJob impl from header to cpp file 2025-06-02 19:45:37 -03:00
David Capello 66123e9d57 Update flic module 2025-05-29 13:39:05 -03:00
David Capello f1b6dd8594 [lua] Fix regression overwriting path specified in Dialog:file{filename} (fix #5061)
If no 'basepath' is specified and the 'filename' argument has a path,
we must use that path as 'basepath' to keep backward compatibility
with the previous behavior of Dialog:file{}.
2025-05-22 14:39:36 -03:00
David Capello 79de4da82a Update laf module 2025-05-22 09:06:35 -03:00
David Capello 9c3f985ee5 [build] Remove xargs and echo -n (fix #5179?)
This is a test to check if "echo -n" and "xargs" are necessary.
2025-05-21 21:28:24 -03:00
Reese Rivers 2cb42b343f Add missing Esperanto accents (ŜŝŬŭ) 2025-05-21 13:56:51 -03:00
Gaspar Capello 38b5f3b283 Fix in Export As dialog the '-export' string never included (fix #5035) 2025-05-19 18:34:58 -03:00
David Capello 41a8249afd New AUTHORS file 2025-05-11 12:14:01 -03:00
David Capello 5dae7e203f Add a summary of open source projects in LICENSES file 2025-05-11 10:26:35 -03:00
David Capello d8e8074345 Fix some ToolBar issues calculating the required min height
* The calculation was moved to onResize() event (when bounds are set)
  instead of kPaintMessage
* All groups can be collapsed: in case the available space is so
  small that we can only show one button to expand all tools
* Don't calculate the min height using the "lastToolBounds" absolute
  position, we have to adjust that coordinate with the ToolBar origin
* Make the min height calculation a little more accurate
2025-05-09 21:51:26 -03:00
David Capello 95d65d8163 Update json11 module (#5158) 2025-05-07 17:09:42 -03:00
David Capello a8e190a133 [ui] Fit main menu bar in main window width 2025-05-01 15:35:14 -03:00
David Capello 7c74619c94 Update building instructions
Update SDK versions, use libstdc++ on Linux (as we're only pre-compiling
Skia with libstdc++), add some initial information about build.sh
script.
2025-04-29 12:17:41 -03:00
David Capello 5678086310 Update laf module 2025-04-28 19:23:40 -03:00
David Capello 8158345cea Allow to use the close button when a popup window/menu box is displayed (fix #5111, #5134) 2025-04-28 19:22:54 -03:00
David Capello 06d3bbf953 Integrate fixup_image_transparent_colors() in resize_image() (#5048)
With this patch we've even fixed a couple of bugs:

1. In resize_image() where fixup_image_transparent_colors() was being
   called in the new image instead of the source image.

2. When a bilinear resize was done to a tileset in SpriteSizeCommand,
   each tile was being resized directly without calling
   fixup_image_transparent_colors().
2025-04-24 20:58:16 -03:00
lightovernight 6aabfef0b8 Fix Un-replaceable transparent pixels created by resize (fix #5032, #5048) 2025-04-24 20:37:11 -03:00
David Capello 206065fb80 Add a way to change default Timeline options
Fixes https://community.aseprite.org/t/19863
Related to #5083 / https://community.aseprite.org/t/25097
and #1485 in some way, although we've opted to avoid moving these
options to the Preferences dialog as it's quite a big refactor.
2025-04-24 19:38:25 -03:00
cybardev 90be6aac30 [build] Use local .deps directory for users (fix #4998)
For an user setup we'd prefer to download Skia inside a .deps
directory (just to simplify the setup). For developers it's better if
we offer a common/shared/absolute directory so different local
Aseprite clones can share the same downloaded Skia version.

Co-authored-by: David Capello <david@igara.com>
2025-04-24 19:34:09 -03:00
David Capello 600882352e Avoid warning using std::stable_sort() from gcc/clang 2025-04-22 22:23:09 -03:00
David Capello aca46a28c5 Remove unused variables 2025-04-22 21:59:21 -03:00
David Capello b099d8f780 Fix initialization order of DelayedMouseMove members 2025-04-22 21:58:27 -03:00
David Capello 378f7ac6c2 Fix small int type for the given kPinnedLimit value 2025-04-22 21:57:48 -03:00
Martín Capello 3bc84cca53 Fix timeline scrolling when creating layer
Fix #4930
2025-04-22 21:54:22 -03:00
Gaspar Capello 3549d3538f Fix regression with timeline thumbnails (fix #5083)
This fix adds an option to scale timeline thumbnails to fill
the entire cell, or simply leave the timeline thumbnails at 1:1 scale
as before issue #4974.
2025-04-22 20:18:21 -03:00
Christian Kaiser 39d69ac8cf Palette picker pinning, loading speed, misc improvements (fix #2365) 2025-04-22 12:45:56 -03:00
Martín Capello ba5adcaa7d Change uuidsForLayers by useLayerUuids 2025-04-22 12:42:12 -03:00
Martín Capello 9e35fd817a [lua] Add uuid field to layers (fix #5033) 2025-04-22 12:42:12 -03:00
David Capello 65c2ed6a35 [build] Use platform.sh file after checking submodules
As this script is part of laf submodule, we have to check the existence
of submodules first, and then run the script.

We've also moved the cl.exe check to build.sh directly.
2025-04-22 07:58:34 -03:00
David Capello c59f1825be Remove extra whitespace between in copyright lines 2025-04-21 19:41:02 -03:00
Gaspar Capello 7f07becd74 Fix cel.image:clear() cannot be undone (fix #5015) 2025-04-21 19:39:32 -03:00
Gaspar Capello fdc9b2f000 Fix symmetry button is kept pressed when we didn't pressed (fix #4760)
This fix removes the 'non sense symmetry filter' to prevent
some buttons from being unintentionally held down.

Moved the drawing process for symmetry axes from
'Editor::drawSpriteUnclippedRect' to
'Editor::drawOneSpriteUnclippedRect' to allow semi-transparent axes.
This also produces axes on every tile in tile mode.

Pixel ratios other than 1:1 are now considered in the drawing logic of
diagonal axes.
2025-04-21 19:20:43 -03:00
David Capello 2e37ac9b83 [build] Don't use unsupported ${var,,} syntax on some shells (fix #5082) 2025-04-18 22:43:36 -03:00
David Capello 753d892af2 [build] read -N 1 is not supported on macOS bash 2025-04-18 22:43:18 -03:00
David Capello 8a8ddbc630 Improve performance loading list of fonts using an app::Task
We list the fonts in a background thread to fill the list of fonts in
the UI. And now we are inserting the fonts in alphabetical order.
2025-04-18 19:56:56 -03:00
David Capello 943c3b28df Merge branch 'main' into beta 2025-04-17 21:10:04 -03:00
David Capello d9a138357e Update modules 2025-04-17 20:34:07 -03:00
David Capello 978000a9dd Better support to use system fonts from themes
* There is a new system="" attribute for <font> elements
* New <font><windows ... /><macos ... /><linux /></font> elements to
  select platform specific fonts
* Fixed several bugs related to re-using fonts from the cache when
  customizing the current theme font from Edit > Preferences > Theme
* Removed Fonts::infoFromFont() adding FontInfo(FontData) constructor
2025-04-17 19:43:32 -03:00
David Capello b3f4e37b69 Add option to switch font hinting (fix #4931, aseprite/laf#138) 2025-04-11 17:26:03 -03:00
David Capello d8632b6208 Move misc/ scripts to laf library 2025-04-11 09:03:28 -03:00
David Capello 3c4d012210 [ci] Fix slow tests 2025-04-10 09:56:46 -03:00
David Capello d51a6d4f51 [ci] New build-auto job to test the build.sh script
This runs only when the build.sh (or some misc/ script) is modified.
2025-04-10 09:21:06 -03:00
David Capello 2c9eb2a801 New misc/skia-url.sh script to simplify downloading the required Skia branch 2025-04-10 09:08:06 -03:00
David Capello bae8520580 Update modules due cmake_minimum_required() issues (#5087, #5071) 2025-04-02 21:44:03 -03:00
David Capello 7167969963 [theme] Fix horizontal separator alignment 2025-04-02 09:32:34 -03:00
David Capello 537ccd393f Prevent polling keyboard state for each created ui::Message
Getting the keyboard state to fill the keyboard modifiers can be
expensive (mainly on Windows calling GetAsyncKeyState). So we can lazy
evaluate the modifiers when they are needed.
2025-04-02 08:00:11 -03:00
Christian Kaiser b130601716
Startup optimizations (#5090)
* Delay DitheringSelector startup
* Delay setting the drag target
* Delay & enqueue menu reloading
2025-04-02 07:59:52 -03:00
David Capello 816be744ac Add Widget::textBaseline() to fix several misalignments using scalable fonts
* Each widget can customize its text baseline (onGetTextBaseline()) to
  know where to draw text when it's vertically aligned to the middle
  (now more correctly "aligned to the baseline").
* Add PAINT_BASELINE to test where the baseline is on each widget.
* "Aseprite" and "Aseprite Mini" fonts now have a special
  descent/ascent configuration with a specific baseline position.
2025-03-27 18:22:51 -03:00
David Capello 50bccf85fc Add support to scale sprite sheet fonts
We've highlighted (with an "*") the sprite sheet font size, so it's
easy to select its default size.
2025-03-24 23:19:30 -03:00
David Capello 2ee4819bf9 Show link to go to the Themes section to customize the font for certain languages 2025-03-14 16:02:31 -03:00
David Capello 805d98e943 Fix font rendering issues in non-English languages (fix #4804)
Graphics::measureText() wasn't shaping text correctly using the
FontMgr to get more fonts when they were required. And now
SkinTheme::drawText() can use Widget::textSize() (the cached size of
the widget text blob) to calculate the required text size.
2025-03-13 16:20:11 -03:00
Martín Capello c68b4923f8 Prevent using an invalid drawing point (fix #5055) 2025-03-12 15:47:23 -03:00
David Capello 53c415c933 Fix entry height in ColorSliders when UI Scaling > 100% 2025-03-10 10:02:14 -03:00
David Capello d3265a1711 Fix UI vs Screen Scaling bugs w/window buttons and button sets/grids (fix #5043)
Includes:
* New ui::Style::rawMargin/Border/Padding() to detect what values are
  undefined, and ui::Style::margin/border/padding() to return normalized
  values (replacing kUndefinedSide with 0 when a value is not defined in
  the whole hierarchy of styles). With this change we avoid using a
  margin/border/padding value of -1 by mistake.
* New guiscaled_div() to calculate an integer division taking care the
  guiscale() for a scaled value.
* CALC_FOR_CENTER() renamed to guiscaled_center() and moved to ui/scale.h

There are still a lot of work to be done to fully fix these UI issues
between Screen Scaling=200%/UI Scaling=100% vs Screen Scaling=100%/UI
Scaling=200%
2025-03-09 22:01:55 -03:00
David Capello c949f4a5a6 Centralize defined fonts in new app::Fonts class 2025-03-04 13:54:15 -03:00
David Capello 2706d2d75a Move font related types to src/app/fonts 2025-03-03 22:36:11 -03:00
David Capello ce989684d0 Don't save custom theme fonts when we uncheck these options 2025-03-03 21:48:47 -03:00
David Capello da5909639b Use the new Surface::applyScale() 2025-03-03 16:42:03 -03:00
David Capello fc63532fef Ignore .vscode directory 2025-03-03 11:24:52 -03:00
201 changed files with 4201 additions and 2631 deletions

View File

@ -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

43
.github/workflows/build-auto.yml vendored Normal file
View File

@ -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

View File

@ -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' }}

View File

@ -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 }}

2
.gitignore vendored
View File

@ -12,7 +12,9 @@
*.res
.DS_Store
.vs
.vscode
tests/_test*
build
.build
.deps
CMakeSettings.json

98
AUTHORS.md Normal file
View File

@ -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.

View File

@ -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}

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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">

View File

@ -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

View File

@ -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>

View File

@ -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" />

View File

@ -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

View File

@ -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 &amp;&amp; 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 &amp;&amp; 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" />

View File

@ -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>

View File

@ -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" />

View File

@ -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" />

View File

@ -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>

View File

@ -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

@ -1 +1 @@
Subproject commit 65829107c838817987f3cf6374cc68c583e5d538
Subproject commit 7d30a582e5c8655fab368c4d61f3125855a7b30d

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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));
}

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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();

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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()

View File

@ -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);
}
}

View File

@ -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()) {

View File

@ -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()

View File

@ -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()) {

View File

@ -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);
}

View File

@ -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)));
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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);

View File

@ -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;

View File

@ -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

81
src/app/fonts/font_data.h Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

98
src/app/fonts/fonts.cpp Normal file
View File

@ -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

49
src/app/fonts/fonts.h Normal file
View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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 }
};

View File

@ -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;

View File

@ -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 }
};

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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

View File

@ -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();

View File

@ -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())

View File

@ -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);

View File

@ -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

View File

@ -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());

View File

@ -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

View File

@ -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(); });

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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

View File

@ -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();

View File

@ -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));

View File

@ -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);

View File

@ -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

View File

@ -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;
};

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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;

View File

@ -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

View File

@ -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