Compare commits

...

68 Commits

Author SHA1 Message Date
Azamat H. Hackimov 888fb72b70
Merge 25c629f5ed into e39dc90001 2025-09-19 19:07:35 -03:00
David Capello e39dc90001 [lua] Enclose key names for plugin preferences with ["..."] when it's required (#5412) 2025-09-19 15:27:00 -03:00
Martín Capello 8f0dea0988 Avoid syntax errors reading extension preferences
Fix #5412 by using brackets and double quotes to enclose the name of
the preferences' properties and avoid issues when they contain unicode
chars
2025-09-19 14:58:35 -03:00
David Capello e8f18087e5 Update laf module 2025-09-18 20:47:56 -03:00
David Capello d8ac1a7a9e Update laf and add new libxrandr dependency 2025-09-17 09:15:35 -03:00
Aiden J Erickson 5cfa75ab3f [multimon] allow windows to open in non-default workareas (#4560, #5426)
Fix issue below:
In multi-monitor setups, native windows, e.g. the preview window, will,
when re-opened, be moved/clamped to the monitor of the main aseprite
window even if the re-opened window was previously and explicitly moved
to a different monitor.
2025-09-16 20:42:21 -03:00
David Capello c444b566e1 Fix painting UI theme colors as they are specified in sRGB color space
Fixes a regression found after merging #5414:
https://github.com/aseprite/aseprite/pull/5414#issuecomment-3286339563

We always expected a sRGB color in ui::Graphics API and we can specify
a color in another color space using the ui::Paint version of its
member functions.

Several functions related to color spaces are now using a ui::Display
to receive the specific display where we're going to paint, instead of
using os::System::instance()->defaultWindow()->colorSpace().
2025-09-16 09:42:35 -03:00
David Capello 5d5f3ec234 Convert AppShortcut in a ui::Shortcut subclass 2025-09-12 15:48:38 -03:00
David Capello 71e680e05a Refactor key context matching (fix #5390)
* Added AppShortcut::fitsBetterThan() to compare shortcuts and decide
  which one is better depending on the current key context and the ones
  where shortcuts are defined
* Added Key::fitsContext() to known if this key definition makes sense
  for a given current key context, e.g. Normal context can be used in
  SelectionTool context, and SelectionTool can be used in Transformation
* Added KeyboardShortcuts::findBestKeyFromMessage() to centralize the
  keyboard shortcuts matching moving some code from
  CustomizedGuiManager::processKey()
* Added more keyboard shortcuts tests (including some for #5390)
2025-09-12 15:41:08 -03:00
David Capello df5dcdc1d9 Add some KeyboardShortcuts basic tests 2025-09-12 15:07:26 -03:00
David Capello 85793a9e5f Add AppShortcut to include app::KeySource + ui::Shortcut
We need a list of shorcuts (AppShortcuts) similar to ui::Shortcuts but
including the key source to compare later where the shortcut was
defined and give more priority to the user-defined shortcuts. This
logic is not yet added, this patch is only a refactor.
2025-09-12 15:07:26 -03:00
David Capello d7b07a5173 Now Paint::color() can receive a color space w/any laf backend 2025-09-12 12:24:53 -03:00
Thorbjørn Lindeijer fae42dbe12 CMake: Use find_package(PkgConfig) instead of direct include
This gets rid of the following warning:

```
CMake Warning (dev) at /usr/share/cmake/Modules/FindPackageHandleStandardArgs.cmake:441 (message):
  The package name passed to `find_package_handle_standard_args` (PkgConfig)
  does not match the name of the calling package (HarfBuzz).  This can lead
  to problems in calling code that expects `find_package` result variables
  (e.g., `_FOUND`) to follow a certain pattern.
Call Stack (most recent call first):
  /usr/share/cmake/Modules/FindPkgConfig.cmake:114 (find_package_handle_standard_args)
  aseprite/cmake/FindHarfBuzz.cmake:33 (include)
  CMakeLists.txt:173 (find_package)
This warning is for project developers.  Use -Wno-dev to suppress it.
```

By applying the following change:
407fa892d9

The file could use further updates based on the upstream version.
2025-09-12 11:18:52 -03:00
David Capello 689e1ff524 Remove trailing whitespace from feature_request.md
This was the only file failing a pre-commit run --all-files
2025-09-11 17:59:13 -03:00
Gaspar Capello 907138d5dd Fix wrong color space in multiple UI elements
This fix solves color space missmatch in the following UI elements:
- Timeline thumbnails
- Palette entries
- Tileset entries
- Color Spectrum and Wheels
- Color Popup Gradients
- Color Button
2025-09-05 14:50:36 -03:00
Christian Kaiser af349ce7ee [lua] Fix new layers not going to the top when selected (fix #5364) 2025-09-04 12:02:10 -03:00
David Capello 3bc4ac0838 Fix the "No Hinting" label for Font info description 2025-09-04 11:52:55 -03:00
Christian Kaiser 1774d86939 Reset weight button when weight is not available, show weight in popup 2025-09-04 11:49:57 -03:00
Christian Kaiser dce1dfd06b Add a font weight dropdown to the font selector 2025-09-04 11:49:57 -03:00
adityarana14 6283d69707 [Script] Added support for mousecursor and still supporting mouseCursor
Added the support for both "mousecursor" and "mouseCursor"
2025-09-04 10:51:49 -03:00
David Capello 21ad78cdbb Move app::Key to its own key.cpp file 2025-09-02 15:22:47 -03:00
David Capello 20b11a0fd3 Add image_benchmark 2025-09-01 17:09:03 -03:00
David Capello 8d4c4857ee Add new kind of Image iterators line by line: Image::read/writeArea() 2025-09-01 17:09:03 -03:00
David Capello 70c8924719 Add flag to switch IMAGE_BITMAP to 8bpp instead of 1bpp
We can set DOC_USE_BITMAP_AS_1BPP=0 to use 8bpp to simplify iteration
of bitmap pixel format using a 8bpp buffer just like indexed mode.
2025-09-01 17:09:03 -03:00
David Capello 286dd1c755 Update laf module 2025-08-28 11:56:26 -03:00
Martín Capello 3a14ac72a4 Fix DECORATIVE widget drawing outside of window 2025-08-27 15:33:10 -03:00
Gaspar Capello 2db193b8e3 Fix pivot position when Tiled Mode is enabled (fix #2316) 2025-08-27 15:32:50 -03:00
David Capello 002356ce19 [lua] Avoid accessing nullptr parent display at exit (#5384) 2025-08-27 15:26:31 -03:00
David Capello 9a1e92da35 Fix arrow keys to move selection (fix #5385)
Regression introduced in f61c2c3950
2025-08-27 15:22:42 -03:00
David Capello 07803ff361 Add missing [[fallthrough]] attribute 2025-08-27 12:25:19 -03:00
David Capello 8e07617a9d Fix non-void function path without return 2025-08-27 12:22:01 -03:00
Martín Capello 9c5ca6bcc6 Internationalize " Copy" string usage 2025-08-27 12:16:35 -03:00
Martín Capello e193891df3 Use "Copy" suffix to name duplicated slices 2025-08-27 12:16:35 -03:00
Martín Capello bb8547d004 Offset duplicated slices to avoid overlapping 2025-08-27 12:16:35 -03:00
Martín Capello 85997a08cf Add slices copy&paste and duplication (fix #4466) 2025-08-27 12:16:35 -03:00
Gaspar Capello 0995e72a6f Fix bug in Saturation layer blend mode (fix #2661) 2025-08-27 12:16:05 -03:00
David Capello f61c2c3950 Revert Esc key behavior to drop+deselect (fix #5102)
This adds a new button in the context bar so we have the three
available options to handle a transformation/drop pixels:

* Drop pixels and deselect (Esc key)
* Drop pixels but keep the selection (Enter key), new "Apply" command
* Discard changes/undo (Ctrl+Z)

This adds a new key context (Transformation) and also fixes tooltip
shortcuts on context bar buttons to show the current configured
shortcut for each action.

Reverts debab653fa and 194f8424a8
2025-08-27 11:56:57 -03:00
David Capello 0c49f2d7ad Update both .xml theme files through the tinyxml2 serializer 2025-08-27 09:31:46 -03:00
David Capello 5f7cc42333 Remove unnecessary call to regenerateCols() in Timeline::refresh()
Related to:
https://github.com/aseprite/aseprite/pull/5367#issuecomment-3225287017
2025-08-26 15:39:55 -03:00
Christian Kaiser 8e75cfc4c7 [lua] Refresh timeline when changing layer collapsed status (fix #5366) 2025-08-26 15:35:28 -03:00
David Capello 983b07383f [lua] Fix default autofit for a Dialog() when autofit is not specified (#5176 / #5321) 2025-08-26 11:23:48 -03:00
Christian Kaiser c57554646b Escape characters in the console that we can't show properly (fix #5324) 2025-08-26 11:21:08 -03:00
Christian Kaiser 74953174d6 [lua] Added `autofit` and `sizeHint` properties to Dialog (fix #5176) 2025-08-26 11:05:06 -03:00
Christian Kaiser 49fa35237a Activate the native window when asking the user to save sprite changes (fix #3542 / #5318) 2025-08-26 09:57:51 -03:00
Christian Kaiser 0ccf9dcc4f [lua] Add app.tip (#5316 / #5348) 2025-08-26 09:00:40 -03:00
David Capello 194f8424a8 Add right-click to configure the cancel selection button (#5102 / #5145) 2025-08-26 07:55:14 -03:00
Joshua Lopez debab653fa Fix marquee tool escape deselection (fix #5102) 2025-08-25 16:51:03 -03:00
hwabis 6e9024d54d Fix mouse wheel zooming not working with zoom tool (quick) 2025-08-25 16:35:14 -03:00
David Capello 1fa7fd0831 Minor UI fixes for dialogs with user data
This patch unifies the behavior of all dialogs with user data to
expand the window vertically onToggleUserData().
2025-08-25 16:20:37 -03:00
David Capello ab6b040e83 Rename Mask::m_freeze_count -> Mask::m_freezes 2025-08-25 15:30:23 -03:00
Gaspar Capello bc312a37b3 Fix Color Management regression (fix #5333)
Before this fix, the assigned color space was always sRGB (default),
so this color space was used for the rest of the rendering.
Now the color space for the back layer of the display
has been made explicit.
2025-08-25 13:10:25 -03:00
Christian Kaiser 3129fda977 [lua] Add missing "DIAGONAL" FlipType (fix #5359) 2025-08-17 14:58:26 -03:00
David Capello 6cb61fb41e Remove deprecated issue template 2025-08-14 14:17:56 -03:00
David Capello aa817a8d2a
Update issue templates 2025-08-14 14:11:47 -03:00
David Capello 40031f83d8 [lua] Fix typo in Sprite:newCel() error 2025-08-13 13:06:05 -03:00
David Capello 90282dbc40 Fix cast error in HarfBuzz library between function types
This includes the following patch:
60c6b7786d
2025-08-11 15:29:58 -03:00
Martín Capello 1227f9c49c Refactor getLayerIndexFromSprite into LayerGroup 2025-08-11 14:47:56 -03:00
Martín Capello d61ae919ad Fix crash dropping file on timeline (fix #5289) 2025-08-11 14:47:56 -03:00
David Capello b2b2583176 [lua] Update scripting API version to 35 2025-08-07 20:18:10 -03:00
David Capello b535212642 Update laf module 2025-08-07 20:14:43 -03:00
Christian Kaiser 229a3cdf65 [lua] Add onchecked parameter to Commands 2025-08-07 18:57:28 -03:00
Christian Kaiser b3814ec912 Unify ContextBar updates when moving pixels, tooltips (fix #5329) 2025-08-06 20:20:42 -03:00
David Capello e88f3bb413 Show error if curl/unzip tools aren't available (fix #5309) 2025-08-06 14:44:21 -03:00
Christian Kaiser eaa2bdf0af [lua] Process mnemonics consistently for plugins (fix #5250) 2025-08-04 15:58:29 -03:00
Christian Kaiser 57309e5aa5 Allow gif encoding to be stopped (fix #2619) 2025-08-01 20:46:32 -03:00
Azamat H. Hackimov 25c629f5ed Switch TinyEXIF to use CPM.cmake
Use imported target `TinyEXIF` to simplify including a library to the
project.
2025-07-31 14:46:18 +03:00
Azamat H. Hackimov 224f126b6b Switch tinyxml2 to use CPM.cmake
Use imported target `tinyxml2::tinyxml2` to simplify including a
library to the project.
2025-07-31 14:46:01 +03:00
Azamat H. Hackimov fbd00b2d1a Add CPM.cmake module for dependency handling
CPM provides easy interface for third_party dependency handling, which
includes downloading, building and exporting targets into the project.
2025-07-31 14:37:15 +03:00
121 changed files with 6005 additions and 2894 deletions

View File

@ -1,9 +0,0 @@
Describe your bug report or feature request here
...
...
...
### Aseprite and System version
* Aseprite version: version number, installer/portable/Steam/beta/dev/commit-hash
* System: Windows/macOS/Linux, version, distribution

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug, triage
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots or a screen recording to help explain your problem.
**Aseprite & System (please complete the following information):**
- Aseprite: [version number, installer/portable/Steam/beta/dev/commit-hash]
- System: [Windows/macOS/Linux, version, distribution]
- Extensions: [List the extensions you have installed]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,29 @@
---
name: Feature request
about: Suggest an idea for Aseprite
title: ''
labels: feature, triage
assignees: ''
---
**Did other user suggested a similar idea?**
- [ ] No
- [ ] Yes/Links to similar ideas
> You can try to find a similar feature requests before in:
> - GitHub issues: https://github.com/aseprite/aseprite/issues?q=label%3Afeature
> - Community site: https://community.aseprite.org/c/features/7
> - Steam community: https://steamcommunity.com/app/431730/discussions/1/
> In case you find a similar feature request, making a comment there will be useful to give some traction and show interest in the feature.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -24,7 +24,8 @@ jobs:
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
libx11-dev libxcursor-dev libxi-dev libxrandr-dev libgl1-mesa-dev \
libfontconfig1-dev
- uses: aseprite/get-ninja@main
- uses: ilammy/msvc-dev-cmd@v1
if: runner.os == 'Windows'

View File

@ -28,7 +28,8 @@ jobs:
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
libx11-dev libxcursor-dev libxi-dev libxrandr-dev libgl1-mesa-dev \
libfontconfig1-dev
- name: Install Skia
if: ${{ matrix.ui == 'gui' }}
shell: bash

View File

@ -25,7 +25,7 @@ jobs:
apt_packages: |
libc++-dev, libc++abi-dev, libpixman-1-dev,
libfreetype6-dev, libharfbuzz-dev, zlib1g-dev, libx11-dev,
libxcursor-dev, libxi-dev, libgl1-mesa-dev
libxcursor-dev, libxi-dev, libxrandr-dev, libgl1-mesa-dev
cmake_command: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DLAF_BACKEND=none -DCMAKE_EXPORT_COMPILE_COMMANDS=on
- uses: ZedThree/clang-tidy-review/upload@v0.20.1

3
.gitignore vendored
View File

@ -18,3 +18,6 @@ build
.build
.deps
CMakeSettings.json
# third_party exclusions
third_party/tinyexif
third_party/tinyxml2

6
.gitmodules vendored
View File

@ -75,9 +75,3 @@
[submodule "third_party/qoi"]
path = third_party/qoi
url = https://github.com/aseprite/qoi.git
[submodule "third_party/tinyxml2"]
path = third_party/tinyxml2
url = https://github.com/aseprite/tinyxml2.git
[submodule "third_party/TinyEXIF"]
path = third_party/TinyEXIF
url = https://github.com/aseprite/TinyEXIF.git

View File

@ -54,6 +54,9 @@ if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/laf/CMakeLists.txt" OR
" https://github.com/aseprite/aseprite/blob/main/INSTALL.md#get-the-source-code")
endif()
set(CPM_SOURCE_CACHE third_party)
include(cmake/CPM.cmake)
# This is required for KDE/Qt destop integration, which sets
# BUILD_SHARED_LIBS to TRUE by default
set(BUILD_SHARED_LIBS off)
@ -262,23 +265,25 @@ add_definitions(-DPNG_NO_MMX_CODE) # Do not use MMX optimizations in PNG code
# tinyxml2
if(USE_SHARED_TINYXML)
find_library(TINYXML_LIBRARY NAMES tinyxml2)
find_path(TINYXML_INCLUDE_DIR NAMES tinyxml2.h)
find_package(tinyxml2 REQUIRED)
else()
set(TINYXML_LIBRARY tinyxml2)
set(TINYXML_INCLUDE_DIR ${TINYXML_DIR})
CPMAddPackage(
URI "gh:aseprite/tinyxml2#312a8092245df393db14a0b2427457ed2ba75e1b"
OPTIONS "tinyxml2_BUILD_TESTING OFF"
)
endif()
include_directories(${TINYXML_INCLUDE_DIR})
# TinyEXIF
if(USE_SHARED_TINYEXIF)
find_library(TINYEXIF_LIBRARY NAMES TinyEXIF)
find_path(TINYEXIF_INCLUDE_DIR NAMES TinyEXIF.h)
find_package(TinyEXIF REQUIRED)
set(TINYEXIF_LIBRARY TinyEXIF)
else()
CPMAddPackage(
URI "gh:aseprite/TinyEXIF#21d6cf762724260f8e5bcee4abdee5411ae47813"
OPTIONS "BUILD_SHARED_LIBS OFF" "BUILD_STATIC_LIBS ON" "LINK_CRT_STATIC_LIBS OFF" "BUILD_DEMO OFF"
)
set(TINYEXIF_LIBRARY TinyEXIFstatic)
set(TINYEXIF_INCLUDE_DIR ${TINYEXIF_DIR})
endif()
include_directories(${TINYEXIF_INCLUDE_DIR})
# pixman
if(USE_SHARED_PIXMAN)

View File

@ -74,7 +74,7 @@ On macOS you will need macOS 15.4 SDK and Xcode 16.3 (older versions might work)
You will need the following dependencies on Ubuntu/Debian:
sudo apt-get install -y g++ clang 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 libxrandr-dev libgl1-mesa-dev libfontconfig1-dev
Or use clang-12 packages (or newer) in case that clang in your distribution is older than clang 12.0:
@ -82,15 +82,15 @@ Or use clang-12 packages (or newer) in case that clang in your distribution is o
On Fedora:
sudo dnf install -y gcc-c++ clang libcxx-devel cmake ninja-build libX11-devel libXcursor-devel libXi-devel mesa-libGL-devel fontconfig-devel
sudo dnf install -y gcc-c++ clang libcxx-devel cmake ninja-build libX11-devel libXcursor-devel libXi-devel libXrandr-devel mesa-libGL-devel fontconfig-devel
On Arch:
sudo pacman -S gcc clang cmake ninja libx11 libxcursor mesa-libgl fontconfig libwebp
sudo pacman -S gcc clang cmake ninja libx11 libxcursor libxi libxrandr mesa-libgl fontconfig libwebp
On SUSE:
sudo zypper install gcc-c++ clang 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 libXrandr-devel Mesa-libGL-devel fontconfig-devel
# Automatic Building

View File

@ -428,9 +428,17 @@ if [ ! -d "$skia_library_dir" ] ; then
skia_url=$(bash laf/misc/skia-url.sh $skia_build)
skia_file=$(basename $skia_url)
if [ ! -f "$skia_dir/$skia_file" ] ; then
if ! command -v curl >/dev/null 2>&1 ; then
echo "Error: 'curl' command line tool is not available in PATH"
exit 1
fi
curl --ssl-revoke-best-effort -L -o "$skia_dir/$skia_file" "$skia_url"
fi
if [ ! -d "$skia_library_dir" ] ; then
if ! command -v unzip >/dev/null 2>&1 ; then
echo "Error: 'unzip' command line tool is not available in PATH"
exit 1
fi
unzip -n -d "$skia_dir" "$skia_dir/$skia_file"
fi
else

1363
cmake/CPM.cmake Normal file

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@
# HARFBUZZ_INCLUDE_DIRS - containg the HarfBuzz headers
# HARFBUZZ_LIBRARIES - containg the HarfBuzz library
include(FindPkgConfig)
find_package(PkgConfig QUIET)
pkg_check_modules(PC_HARFBUZZ harfbuzz>=0.9.7)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff

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) 2001-2018 David Capello -->
<gui>
<!-- Keyboard shortcuts -->
@ -371,6 +371,12 @@
<param name="quantity" value="1" />
</key>
<!-- Main selection actions (apply transformation / undo) -->
<key command="DeselectMask" shortcut="Esc" context="Transformation" />
<key command="Apply" shortcut="Enter" context="Transformation" />
<key command="Apply" shortcut="Enter Pad" context="Transformation" />
<key command="Undo" shortcut="Ctrl+Z" mac="Cmd+Z" context="Transformation" />
<!-- Move selection with arrows -->
<key command="MoveMask" shortcut="Left" context="Selection">
<param name="target" value="content" />
@ -1203,6 +1209,7 @@
<menu id="slice_popup_menu">
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
<item command="DuplicateSlice" text="@.duplicate" group="slice_popup_duplicate" />
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
</menu>

View File

@ -206,6 +206,7 @@ AddColor_Background = Background
AddColor_Foreground = Foreground
AddColor_Specific = Specific
AdvancedMode = Advanced Mode
Apply = Apply
AutocropSprite = Trim Sprite
AutocropSprite_ByGrid = Trim Sprite by Grid
BackgroundFromLayer = Background from Layer
@ -265,6 +266,7 @@ Despeckle = Despeckle
DeveloperConsole = Developer Console
DiscardBrush = Discard Brush
DuplicateLayer = Duplicate Layer
DuplicateSlice = Duplicate Slice
DuplicateSprite = Duplicate Sprite
DuplicateView = Duplicate View
Exit = Exit
@ -581,8 +583,9 @@ rotsprite = RotSprite
pixel_perfect = Pixel-perfect
linear_gradient = Linear Gradient
radial_gradient = Radial Gradient
drop_pixel = Drop pixels here (Enter)
cancel_drag = Cancel drag and drop (Esc)
drop_pixel_and_deselect = Apply transformation and deselect
drop_pixel = Apply transformation and keep selection
cancel_drag = Cancel transformation and undo/discard changes
auto_select_layer = Auto Select Layer
all = All
none = None
@ -621,6 +624,14 @@ current_layer = Current Layer
first_ref_layer = First Reference Layer
pick = Pick:
sample = Sample:
position_label = P:
rotation_label = R:
position_x = X Position
position_y = Y Position
size_width = Width
size_height = Height
rotation_angle = Angle
rotation_skew = Skew
[convolution_matrix]
reload_stock = &Reload Stock
@ -789,7 +800,22 @@ empty_fonts = No system fonts were found
[font_style]
antialias = Antialias
hinting = Hinting
hinting_none = No Hinting
hinting_slight = Slight Hinting
hinting_full = Full Hinting
ligatures = Ligatures
font_weight = Font Weight
italic = Italic
font_weight_100 = Thin
font_weight_200 = Extra Light
font_weight_300 = Light
font_weight_400 = Normal
font_weight_500 = Medium
font_weight_600 = Semi Bold
font_weight_700 = Bold
font_weight_800 = Extra Bold
font_weight_900 = Black
font_weight_1000 = Extra Black
[frame_combo]
all_frames = All frames
@ -817,6 +843,7 @@ same_in_all_tools = Same in all Tools
opacity = Opacity:
tolerance = Tolerance:
show_more = Show more...
copy_of = {} Copy
[general_text]
copy = &Copy
@ -957,6 +984,7 @@ key_context_move_tool = Move Tool
key_context_freehand_tool = Freehand Tool
key_context_shape_tool = Shape Tool
key_context_frames_selection = Frames Selection
key_context_transformation = Transformation
copy_selection = Copy Selection
snap_to_grid = Snap To Grid
lock_axis = Lock Axis
@ -1664,6 +1692,10 @@ from = From:
to = To:
tolerance = Tolerance:
[duplicate_slice]
x_duplicated = Slice "{}" duplicated
n_slices_duplicated = {} slice(s) duplicated
[remove_slice]
x_removed = Slice "{}" removed
n_slices_removed = {} slice(s) removed
@ -1748,6 +1780,7 @@ delete_file = Delete file, I've already sent it
[slice_popup_menu]
properties = Slice &Properties...
duplicate = D&uplicate Slice
delete = &Delete Slice
[slice_properties]

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2019-2024 Igara Studio S.A. -->
<!-- Copyright (C) 2019-2025 Igara Studio S.A. -->
<!-- Copyright (C) 2017-2018 David Capello -->
<gui>
<window id="slice_properties" text="@.title" help="slices#slice-properties">
<vbox>
<vbox expansive="true">
<grid id="properties_grid" columns="3">
<label id="label1" text="@.name" />
<entry id="name" maxsize="256" magnet="true" cell_align="horizontal" expansive="true" />
<button id="user_data" icon="icon_user_data" maxsize="32" tooltip="@.user_data_tooltip" />
</grid>
<grid columns="2">
<separator horizontal="true" cell_hspan="2" />
<grid columns="3" expansive="true">
<separator horizontal="true" cell_hspan="3" />
<box />
<hbox homogeneous="true">
@ -20,6 +20,7 @@
<label text="@.width" />
<label text="@.height" />
</hbox>
<boxfiller cell_align="horizontal" />
<label text="@.bounds" />
<hbox homogeneous="true">
@ -28,6 +29,7 @@
<expr id="bounds_w" />
<expr id="bounds_h" />
</hbox>
<boxfiller />
<check text="@.center" id="center" />
<hbox homogeneous="true">
@ -36,16 +38,18 @@
<expr id="center_w" />
<expr id="center_h" />
</hbox>
<boxfiller />
<check text="@.pivot" id="pivot" />
<hbox>
<expr id="pivot_x" />
<expr id="pivot_y" />
</hbox>
<boxfiller />
<separator horizontal="true" cell_hspan="2" />
<hbox cell_hspan="2">
<boxfiller cell_align="vertical" cell_hspan="3" />
<separator horizontal="true" cell_hspan="3" cell_align="horizontal" />
<hbox cell_hspan="3">
<boxfiller />
<hbox homogeneous="true">
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2019-2021 Igara Studio S.A. -->
<!-- Copyright (C) 2019-2025 Igara Studio S.A. -->
<!-- Copyright (C) 2015-2018 David Capello -->
<gui>
<window id="tag_properties" text="@.title">
@ -23,13 +23,14 @@
<check text="@.repeat" id="limit_repeat" />
<vbox id="repeat_placeholder" cell_hspan="2" />
</grid>
<boxfiller />
<grid columns="2">
<separator horizontal="true" cell_hspan="2" minwidth="180" />
<separator horizontal="true" cell_align="horizontal" cell_hspan="2" minwidth="180" />
<box horizontal="true" homogeneous="true" cell_hspan="2" cell_align="right">
<hbox homogeneous="true" cell_hspan="2" cell_align="right">
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
<button text="@general.cancel" closewindow="true" />
</box>
</hbox>
</grid>
</vbox>
</window>

View File

@ -2,6 +2,7 @@ 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)
* [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake)
* [cityhash](https://github.com/google/cityhash)
* [cmark](https://github.com/jgm/cmark)
* [curl](http://curl.haxx.se/)
@ -82,6 +83,30 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
# [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake)
```
Copyright (c) 2019-2022 Lars Melchior and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
# [cityhash](https://github.com/google/cityhash)
```

2
laf

@ -1 +1 @@
Subproject commit 8ec4b553f1618f7a4b47cdcf4cfc2663266111ac
Subproject commit de781a5066732d700ac4520ee5fc9034e92875ad

View File

@ -220,6 +220,7 @@ if(ENABLE_TESTS)
find_tests(ui ui-lib)
find_tests(app/cli app-lib)
find_tests(app/file app-lib)
find_tests(app/ui app-lib)
find_tests(app app-lib)
find_tests(. app-lib)
endif()

View File

@ -74,7 +74,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${output_fn}.tmp ${output_fn}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
MAIN_DEPENDENCY ${strings_en_ini}
DEPENDS ${GEN_DEP})
DEPENDS ${GEN_DEP} commands/commands_list.h)
list(APPEND generated_files ${output_fn})
# Check translations
@ -368,6 +368,7 @@ target_sources(app-lib PRIVATE
color_picker.cpp
color_spaces.cpp
color_utils.cpp
commands/apply.cpp
commands/cmd_about.cpp
commands/cmd_add_color.cpp
commands/cmd_advanced_mode.cpp
@ -393,6 +394,7 @@ target_sources(app-lib PRIVATE
commands/cmd_deselect_mask.cpp
commands/cmd_discard_brush.cpp
commands/cmd_duplicate_layer.cpp
commands/cmd_duplicate_slice.cpp
commands/cmd_duplicate_sprite.cpp
commands/cmd_duplicate_view.cpp
commands/cmd_enter_license.cpp
@ -651,6 +653,7 @@ target_sources(app-lib PRIVATE
ui/icon_button.cpp
ui/incompat_file_window.cpp
ui/input_chain.cpp
ui/key.cpp
ui/keyboard_shortcuts.cpp
ui/layer_frame_comboboxes.cpp
ui/main_menu_bar.cpp
@ -715,6 +718,7 @@ target_sources(app-lib PRIVATE
util/render_text.cpp
util/resize_image.cpp
util/shader_helpers.cpp
util/slice_utils.cpp
util/tile_flags_utils.cpp
util/tileset_utils.cpp
util/wrap_point.cpp
@ -764,7 +768,7 @@ target_link_libraries(app-lib
updater-lib
undo
${CMARK_LIBRARIES}
${TINYXML_LIBRARY}
tinyxml2::tinyxml2
${TINYEXIF_LIBRARY}
${GIF_LIBRARIES}
${PNG_LIBRARIES}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -15,6 +15,7 @@
#include "app/ui/editor/editor.h"
#include "os/system.h"
#include "os/window.h"
#include "ui/display.h"
namespace app {
@ -29,17 +30,15 @@ void initialize_color_spaces(Preferences& pref)
pref.color.manage.AfterChange.connect([](bool manage) { g_manage = manage; });
}
os::ColorSpaceRef get_screen_color_space()
os::ColorSpaceRef get_current_color_space(ui::Display* display, Doc* doc)
{
return os::System::instance()->defaultWindow()->colorSpace();
}
os::ColorSpaceRef get_current_color_space()
{
if (auto* editor = Editor::activeEditor())
return editor->document()->osColorSpace();
else
return get_screen_color_space();
if (!doc) {
if (auto* editor = Editor::activeEditor())
doc = editor->document();
}
if (doc)
return doc->osColorSpace();
return display->colorSpace();
}
gfx::ColorSpaceRef get_working_rgb_space_from_preferences()
@ -62,11 +61,11 @@ gfx::ColorSpaceRef get_working_rgb_space_from_preferences()
//////////////////////////////////////////////////////////////////////
// Color conversion
ConvertCS::ConvertCS()
ConvertCS::ConvertCS(ui::Display* display, Doc* doc)
{
if (g_manage) {
auto srcCS = get_current_color_space();
auto dstCS = get_screen_color_space();
auto srcCS = get_current_color_space(display, doc);
auto dstCS = display->colorSpace();
if (srcCS && dstCS)
m_conversion = os::System::instance()->convertBetweenColorSpace(srcCS, dstCS);
}
@ -95,9 +94,9 @@ gfx::Color ConvertCS::operator()(const gfx::Color c)
}
}
ConvertCS convert_from_current_to_screen_color_space()
ConvertCS convert_from_current_to_display_color_space(ui::Display* display)
{
return ConvertCS();
return ConvertCS(display);
}
ConvertCS convert_from_custom_to_srgb(const os::ColorSpaceRef& from)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2018-2020 Igara Studio S.A.
// Copyright (c) 2018-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -16,21 +16,25 @@ namespace doc {
class Sprite;
}
namespace ui {
class Display;
}
namespace app {
class Doc;
class Preferences;
void initialize_color_spaces(Preferences& pref);
os::ColorSpaceRef get_screen_color_space();
// Returns the color space of the current document.
os::ColorSpaceRef get_current_color_space();
os::ColorSpaceRef get_current_color_space(ui::Display* display, Doc* doc = nullptr);
gfx::ColorSpaceRef get_working_rgb_space_from_preferences();
class ConvertCS {
public:
ConvertCS();
ConvertCS() = delete;
ConvertCS(ui::Display* display, Doc* doc = nullptr);
ConvertCS(const os::ColorSpaceRef& srcCS, const os::ColorSpaceRef& dstCS);
ConvertCS(ConvertCS&&);
ConvertCS& operator=(const ConvertCS&) = delete;
@ -40,7 +44,7 @@ private:
os::Ref<os::ColorSpaceConversion> m_conversion;
};
ConvertCS convert_from_current_to_screen_color_space();
ConvertCS convert_from_current_to_display_color_space(ui::Display* display);
ConvertCS convert_from_custom_to_srgb(const os::ColorSpaceRef& from);
} // namespace app

View File

@ -0,0 +1,46 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/commands/command.h"
#include "app/context.h"
#include "app/ui/editor/editor.h"
namespace app {
// Depends on the current context/state, used to apply the current
// transformation (drop pixels).
class ApplyCommand : public Command {
public:
ApplyCommand();
protected:
void onExecute(Context* ctx) override;
};
ApplyCommand::ApplyCommand() : Command(CommandId::Apply(), CmdUIOnlyFlag)
{
}
void ApplyCommand::onExecute(Context* ctx)
{
if (!ctx->isUIAvailable())
return;
auto* editor = Editor::activeEditor();
if (editor && editor->isMovingPixels())
editor->dropMovingPixels();
}
Command* CommandFactory::createApplyCommand()
{
return new ApplyCommand;
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2023 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
@ -101,12 +101,12 @@ public:
if (countCels() > 0) {
m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()),
g_window->propertiesGrid());
propertiesGrid());
}
else if (!m_cel)
m_userDataView.setVisible(false, false);
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
expandWindow(gfx::Size(bounds().w, sizeHint().h));
updateFromCel();
}
@ -281,7 +281,7 @@ private:
{
if (countCels() > 0) {
m_userDataView.toggleVisibility();
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
}

View File

@ -0,0 +1,122 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/cmd/add_slice.h"
#include "app/commands/command.h"
#include "app/context.h"
#include "app/context_access.h"
#include "app/context_flags.h"
#include "app/i18n/strings.h"
#include "app/site.h"
#include "app/tx.h"
#include "app/ui/status_bar.h"
#include "app/util/slice_utils.h"
#include "base/convert_to.h"
#include "doc/object_id.h"
#include "doc/slice.h"
namespace app {
// Moves the given slice by the dx and dy values
void offset(Slice* slice, int dx, int dy)
{
for (auto it = slice->begin(); it != slice->end(); ++it) {
auto* sk = (*it).value();
gfx::Rect bounds = sk->bounds();
bounds.offset(gfx::Point{ dx, dy });
sk->setBounds(bounds);
}
}
class DuplicateSliceCommand : public Command {
public:
DuplicateSliceCommand();
protected:
void onLoadParams(const Params& params) override;
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
private:
ObjectId m_sliceId;
};
DuplicateSliceCommand::DuplicateSliceCommand()
: Command(CommandId::DuplicateSlice(), CmdRecordableFlag)
{
}
void DuplicateSliceCommand::onLoadParams(const Params& params)
{
std::string id = params.get("id");
if (!id.empty())
m_sliceId = ObjectId(base::convert_to<doc::ObjectId>(id));
else
m_sliceId = NullId;
}
bool DuplicateSliceCommand::onEnabled(Context* context)
{
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasActiveSprite | ContextFlags::HasActiveLayer);
}
void DuplicateSliceCommand::onExecute(Context* context)
{
std::vector<Slice*> selectedSlices;
{
const ContextReader reader(context);
if (m_sliceId == NullId) {
selectedSlices = get_selected_slices(reader.site());
if (selectedSlices.empty())
return;
}
else
selectedSlices.push_back(reader.sprite()->slices().getById(m_sliceId));
}
ContextWriter writer(context);
Tx tx(writer, "Duplicate Slice");
Sprite* sprite = writer.site().sprite();
Doc* doc = static_cast<Doc*>(sprite->document());
doc->notifyBeforeSlicesDuplication();
for (auto* s : selectedSlices) {
Slice* slice = new Slice(*s);
slice->setName(Strings::general_copy_of(slice->name()));
// Offset a bit the duplicated slice to avoid overlapping
offset(slice, 2, 2);
tx(new cmd::AddSlice(sprite, slice));
doc->notifySliceDuplicated(slice);
}
tx.commit();
std::string sliceName;
if (selectedSlices.size() == 1)
sliceName = selectedSlices[0]->name();
StatusBar::instance()->invalidate();
if (!sliceName.empty()) {
StatusBar::instance()->showTip(1000, Strings::duplicate_slice_x_duplicated(sliceName));
}
else {
StatusBar::instance()->showTip(
1000,
Strings::duplicate_slice_n_slices_duplicated(selectedSlices.size()));
}
}
Command* CommandFactory::createDuplicateSliceCommand()
{
return new DuplicateSliceCommand;
}
} // namespace app

View File

@ -225,7 +225,7 @@ private:
LockButtons lock(this);
// 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];
Shortcut shortcut = m_key->shortcuts()[index];
if (ui::Alert::show(Strings::alerts_delete_shortcut(shortcut.toString())) != 1)
return;
@ -269,7 +269,7 @@ private:
if (m_key && m_key->keycontext() != KeyContext::Any) {
int w = m_headerItem->contextXPos() +
font()->textLength(convertKeyContextToUserFriendlyString(m_key->keycontext()));
font()->textLength(convert_keycontext_to_user_friendly_string(m_key->keycontext()));
size.w = std::max(size.w, w);
}
@ -317,7 +317,7 @@ private:
if (m_key && !m_key->shortcuts().empty()) {
if (m_key->keycontext() != KeyContext::Any) {
g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()),
g->drawText(convert_keycontext_to_user_friendly_string(m_key->keycontext()),
fg,
bg,
gfx::Point(contextXPos, y));
@ -329,7 +329,7 @@ private:
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->shortcuts().size()));
if (clip) {
int i = 0;
for (const Shortcut& shortcut : m_key->shortcuts()) {
for (const AppShortcut& shortcut : m_key->shortcuts()) {
if (i != m_hotShortcut || !m_changeButton) {
g->drawText(getShortcutText(shortcut), fg, bg, gfx::Point(keyXPos, y));
}
@ -362,7 +362,7 @@ private:
gfx::Rect bounds = this->bounds();
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
const Shortcuts* shortcuts = (m_key ? &m_key->shortcuts() : NULL);
const AppShortcuts* shortcuts = (m_key ? &m_key->shortcuts() : nullptr);
int y = bounds.y;
int dh = textSize().h + 4 * guiscale();
int maxi = (shortcuts && shortcuts->size() > 1 ? shortcuts->size() : 1);
@ -457,7 +457,7 @@ private:
m_hotShortcut = -1;
}
std::string getShortcutText(const Shortcut& shortcut) const
std::string getShortcutText(const AppShortcut& shortcut) const
{
if (m_key && m_key->type() == KeyType::WheelAction && shortcut.isEmpty()) {
return Strings::keyboard_shortcuts_default_action();
@ -595,7 +595,7 @@ private:
case KeyContext::MoveTool:
case KeyContext::FreehandTool:
case KeyContext::ShapeTool:
text = convertKeyContextToUserFriendlyString(key->keycontext()) + ": " + text;
text = convert_keycontext_to_user_friendly_string(key->keycontext()) + ": " + text;
break;
}
KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key, nullptr, 0, &m_headerItem);

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
@ -166,7 +166,7 @@ public:
m_document->add_observer(this);
if (countLayers() > 0) {
m_userDataView.configureAndSet(m_layer->userData(), g_window->propertiesGrid());
m_userDataView.configureAndSet(m_layer->userData(), propertiesGrid());
if (m_remapAfterConfigure) {
remapWindow();
centerWindow();
@ -368,8 +368,7 @@ private:
{
if (m_layer) {
m_userDataView.toggleVisibility();
g_window->remapWindow();
manager()->invalidate();
expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
}

View File

@ -267,16 +267,8 @@ void NewLayerCommand::onExecute(Context* context)
bool afterBackground = false;
switch (m_type) {
case Type::Layer:
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.newGroupAfter(parent, name, activeLayer); break;
case Type::Layer: layer = api.newLayer(parent, name); break;
case Type::Group: layer = api.newGroup(parent, name); break;
case Type::ReferenceLayer:
layer = api.newLayer(parent, name);
if (layer)
@ -311,6 +303,15 @@ void NewLayerCommand::onExecute(Context* context)
ASSERT(layer->parent());
// Reorder the resulting layer.
switch (m_place) {
case Place::AfterActiveLayer: api.restackLayerAfter(layer, parent, activeLayer); break;
case Place::BeforeActiveLayer: api.restackLayerBefore(layer, parent, activeLayer); break;
case Place::Top:
api.restackLayerAfter(layer, sprite->root(), sprite->root()->lastLayer());
break;
}
// Put new layer as an overlay of the background or in the first
// layer in case the sprite is transparent.
if (afterBackground) {

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
@ -188,8 +188,7 @@ private:
void onToggleUserData()
{
m_userDataView.toggleVisibility();
remapWindow();
manager()->invalidate();
expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
void onTilesedDuplicated(const Tileset* tilesetClone)

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
@ -8,6 +8,7 @@
FOR_EACH_COMMAND(About)
FOR_EACH_COMMAND(AddColor)
FOR_EACH_COMMAND(AdvancedMode)
FOR_EACH_COMMAND(Apply)
FOR_EACH_COMMAND(AutocropSprite)
FOR_EACH_COMMAND(BackgroundFromLayer)
FOR_EACH_COMMAND(BrightnessContrast)
@ -40,6 +41,7 @@ FOR_EACH_COMMAND(DeselectMask)
FOR_EACH_COMMAND(Despeckle)
FOR_EACH_COMMAND(DiscardBrush)
FOR_EACH_COMMAND(DuplicateLayer)
FOR_EACH_COMMAND(DuplicateSlice)
FOR_EACH_COMMAND(DuplicateSprite)
FOR_EACH_COMMAND(DuplicateView)
FOR_EACH_COMMAND(Exit)

View File

@ -81,7 +81,7 @@ public:
~ConsoleWindow() { TRACE_CON("CON: ~ConsoleWindow this=", this); }
void addMessage(const std::string& msg)
void addMessage(std::string msg)
{
if (!m_hasText) {
m_hasText = true;
@ -93,6 +93,17 @@ public:
gfx::Point pt = m_view.viewScroll();
const bool autoScroll = (pt.y >= maxSize.h - visible.h);
// Escape characters we can't show properly
for (size_t i = 0; i < msg.size(); i++) {
switch (msg[i]) {
case '\a':
case '\b':
case '\r':
case '\t':
case '\v': msg[i] = ' ';
}
}
m_textbox.setText(m_textbox.text() + msg);
if (autoScroll) {

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
@ -338,6 +338,19 @@ void Doc::notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti)
notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev);
}
void Doc::notifyBeforeSlicesDuplication()
{
DocEvent ev(this);
notify_observers<DocEvent&>(&DocObserver::onBeforeSlicesDuplication, ev);
}
void Doc::notifySliceDuplicated(Slice* slice)
{
DocEvent ev(this);
ev.slice(slice);
notify_observers<DocEvent&>(&DocObserver::onSliceDuplicated, ev);
}
bool Doc::isModified() const
{
return !m_undo->isInSavedStateOrSimilar();

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
@ -141,6 +141,8 @@ public:
void notifyTilesetChanged(Tileset* tileset);
void notifyLayerGroupCollapseChange(Layer* layer);
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
void notifyBeforeSlicesDuplication();
void notifySliceDuplicated(Slice* slice);
//////////////////////////////////////////////////////////////////////
// File related properties

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
@ -90,6 +90,8 @@ public:
// Slices
virtual void onSliceNameChange(DocEvent& ev) {}
virtual void onBeforeSlicesDuplication(DocEvent& ev) {}
virtual void onSliceDuplicated(DocEvent& ev) {}
// The tileset has changed.
virtual void onTilesetChanged(DocEvent& ev) {}

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) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -42,6 +42,7 @@
#include "archive_entry.h"
#include "json11.hpp"
#include <cctype>
#include <fstream>
#include <queue>
#include <sstream>
@ -535,6 +536,19 @@ void Extension::updateCategory(const Category newCategory)
#ifdef ENABLE_SCRIPTING
static bool is_simple_id(const char* k)
{
if (!*k)
return false;
if (!std::isalpha(*k) && *k != '_')
return false;
for (const char* p = k + 1; *p; ++p) {
if (!std::isalnum(*p) && *p != '_')
return false;
}
return true;
}
// TODO move this to app/script/tableutils.h
static void serialize_table(lua_State* L, int idx, std::string& result)
{
@ -555,7 +569,21 @@ static void serialize_table(lua_State* L, int idx, std::string& result)
// Save key
if (lua_type(L, -2) == LUA_TSTRING) {
if (const char* k = lua_tostring(L, -2)) {
result += k;
// If this is a simple identifier we can avoid using ["..."] to enclose
// the key name.
if (is_simple_id(k)) {
result += k;
}
// In other cases, we enclose the key name in brackets and
// double quotes ["..."] to avoid syntax errors when the name
// has unicode characters.
else {
result.push_back('[');
result.push_back('"');
result += k;
result.push_back('"');
result.push_back(']');
}
result.push_back('=');
}
}

View File

@ -1095,6 +1095,9 @@ public:
gifframe_t nframes = totalFrames();
for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) {
ASSERT(frame_it != frame_end);
if (m_fop->isStop())
break;
frame_t frame = *frame_it;
++frame_it;

View File

@ -11,6 +11,7 @@
#include "app/fonts/font_info.h"
#include "app/fonts/font_data.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "base/fs.h"
#include "base/split_string.h"
@ -142,19 +143,22 @@ std::string FontInfo::humanString() const
}
result += fmt::format(" {}pt", size());
if (!result.empty()) {
if (style().weight() >= text::FontStyle::Weight::SemiBold)
result += " Bold";
if (style().weight() != text::FontStyle::Weight::Normal)
result +=
" " +
Strings::Translate(
fmt::format("font_style.font_weight_{}", static_cast<int>(style().weight())).c_str());
if (style().slant() != text::FontStyle::Slant::Upright)
result += " Italic";
result += " " + Strings::font_style_italic();
if (antialias())
result += " Antialias";
result += " " + Strings::font_style_antialias();
if (ligatures())
result += " Ligatures";
result += " " + Strings::font_style_ligatures();
switch (hinting()) {
case text::FontHinting::None: result += " No Hinting"; break;
case text::FontHinting::Slight: result += " Slight Hinting"; break;
case text::FontHinting::None: result += " " + Strings::font_style_hinting_none(); break;
case text::FontHinting::Slight: result += " " + Strings::font_style_hinting_slight(); break;
case text::FontHinting::Normal: break;
case text::FontHinting::Full: result += " Full Hinting"; break;
case text::FontHinting::Full: result += " " + Strings::font_style_hinting_full(); break;
}
}
return result;
@ -177,6 +181,7 @@ app::FontInfo convert_to(const std::string& from)
bool italic = false;
app::FontInfo::Flags flags = app::FontInfo::Flags::None;
text::FontHinting hinting = text::FontHinting::Normal;
text::FontStyle::Weight weight = text::FontStyle::Weight::Normal;
if (!parts.empty()) {
if (parts[0].compare(0, 5, "file=") == 0) {
@ -214,16 +219,17 @@ app::FontInfo convert_to(const std::string& from)
else if (hintingStr == "full")
hinting = text::FontHinting::Full;
}
else if (parts[i].compare(0, 7, "weight=") == 0) {
std::string weightStr = parts[i].substr(7);
weight = static_cast<text::FontStyle::Weight>(std::atoi(weightStr.c_str()));
}
}
}
text::FontStyle style;
if (bold && italic)
style = text::FontStyle::BoldItalic();
else if (bold)
style = text::FontStyle::Bold();
else if (italic)
style = text::FontStyle::Italic();
const text::FontStyle style(
bold ? text::FontStyle::Weight::Bold : weight,
text::FontStyle::Width::Normal,
italic ? text::FontStyle::Slant::Italic : text::FontStyle::Slant::Upright);
return app::FontInfo(type, name, size, style, flags, hinting);
}
@ -243,7 +249,7 @@ std::string convert_to(const app::FontInfo& from)
if (!result.empty()) {
if (from.size() > 0.0f)
result += fmt::format(",size={}", from.size());
if (from.style().weight() >= text::FontStyle::Weight::SemiBold)
if (from.style().weight() == text::FontStyle::Weight::Bold)
result += ",bold";
if (from.style().slant() != text::FontStyle::Slant::Upright)
result += ",italic";
@ -262,6 +268,8 @@ std::string convert_to(const app::FontInfo& from)
case text::FontHinting::Full: result += "full"; break;
}
}
if (from.style().weight() != text::FontStyle::Weight::Bold)
result += ",weight=" + std::to_string(static_cast<int>(from.style().weight()));
}
return result;
}

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
@ -30,6 +30,7 @@
#include "os/surface.h"
#include "os/system.h"
#include "ui/intern.h"
#include "ui/paint.h"
#include "ui/system.h"
#include "ui/theme.h"
@ -118,9 +119,6 @@ void draw_color(ui::Graphics* g,
app::Color color = _color;
const int alpha = color.getAlpha();
// Color space conversion
auto convertColor = convert_from_current_to_screen_color_space();
if (alpha < 255) {
if (rc.w == rc.h)
draw_checkered_grid(g, rc, gfx::Size(rc.w / 2, rc.h / 2));
@ -133,11 +131,15 @@ void draw_color(ui::Graphics* g,
color = app::Color::fromGray(color.getGray(), color.getAlpha());
}
// The color is in the current sprite color space.
ui::Paint paint;
paint.color(color_utils::color_for_ui(color), get_current_color_space(g->display()).get());
if (color.getType() == app::Color::IndexType) {
int index = color.getIndex();
if (index >= 0 && index < get_current_palette()->size()) {
g->fillRect(convertColor(color_utils::color_for_ui(color)), rc);
g->drawRect(rc, paint);
}
else {
g->fillRect(gfx::rgba(0, 0, 0), rc);
@ -147,7 +149,7 @@ void draw_color(ui::Graphics* g,
}
}
else {
g->fillRect(convertColor(color_utils::color_for_ui(color)), rc);
g->drawRect(rc, paint);
}
}
}
@ -219,7 +221,8 @@ void draw_tile(ui::Graphics* g, const Rect& rc, const Site& site, doc::tile_t ti
int w = tileImage->width();
int h = tileImage->height();
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(w, h);
os::SurfaceRef surface =
os::System::instance()->makeRgbaSurface(w, h, get_current_color_space(g->display()));
convert_image_to_surface(tileImage.get(), get_current_palette(), surface.get(), 0, 0, 0, 0, w, h);
ui::Paint paint;

View File

@ -406,7 +406,7 @@ void load_window_pos(Window* window, const char* section, const bool limitMinSiz
if (get_multiple_displays()) {
Rect frame = get_config_rect(section, "WindowFrame", gfx::Rect());
if (!frame.isEmpty()) {
limit_with_workarea(parentDisplay, frame);
limit_least(frame);
window->loadNativeFrame(frame);
}
}
@ -680,82 +680,77 @@ void CustomizedGuiManager::onNewDisplayConfiguration(Display* display)
bool CustomizedGuiManager::processKey(Message* msg)
{
App* app = App::instance();
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
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, contexts[i])) {
// Cancel menu-bar loops (to close any popup menu)
app->mainWindow()->getMenuBar()->cancelMenuLoop();
const KeyPtr key = keys->findBestKeyFromMessage(msg);
if (!key)
return false;
switch (key->type()) {
case KeyType::Tool: {
tools::Tool* current_tool = app->activeTool();
tools::Tool* select_this_tool = key->tool();
tools::ToolBox* toolbox = app->toolBox();
std::vector<tools::Tool*> possibles;
// Cancel menu-bar loops (to close any popup menu)
App* app = App::instance();
app->mainWindow()->getMenuBar()->cancelMenuLoop();
// Collect all tools with the pressed keyboard-shortcut
for (tools::Tool* tool : *toolbox) {
const KeyPtr key = keys->tool(tool);
if (key && key->isPressed(msg))
possibles.push_back(tool);
}
switch (key->type()) {
case KeyType::Tool: {
tools::Tool* current_tool = app->activeTool();
tools::Tool* select_this_tool = key->tool();
tools::ToolBox* toolbox = app->toolBox();
std::vector<tools::Tool*> possibles;
if (possibles.size() >= 2) {
bool done = false;
// Collect all tools with the pressed keyboard-shortcut
for (tools::Tool* tool : *toolbox) {
const KeyPtr key = keys->tool(tool);
if (key && key->isPressed(msg))
possibles.push_back(tool);
}
for (size_t i = 0; i < possibles.size(); ++i) {
if (possibles[i] != current_tool &&
ToolBar::instance()->isToolVisible(possibles[i])) {
select_this_tool = possibles[i];
done = true;
break;
}
}
if (possibles.size() >= 2) {
bool done = false;
if (!done) {
for (size_t i = 0; i < possibles.size(); ++i) {
// If one of the possibilities is the current tool
if (possibles[i] == current_tool) {
// We select the next tool in the possibilities
select_this_tool = possibles[(i + 1) % possibles.size()];
break;
}
}
}
}
ToolBar::instance()->selectTool(select_this_tool);
return true;
}
case KeyType::Command: {
Command* command = key->command();
// Commands are executed only when the main window is
// the current window running.
if (getForegroundWindow() == app->mainWindow()) {
// OK, so we can execute the command represented
// by the pressed-key in the message...
UIContext::instance()->executeCommandFromMenuOrShortcut(command, key->params());
return true;
}
break;
}
case KeyType::Quicktool: {
// Do nothing, it is used in the editor through the
// KeyboardShortcuts::getCurrentQuicktool() function.
for (size_t i = 0; i < possibles.size(); ++i) {
if (possibles[i] != current_tool && ToolBar::instance()->isToolVisible(possibles[i])) {
select_this_tool = possibles[i];
done = true;
break;
}
}
break;
if (!done) {
for (size_t i = 0; i < possibles.size(); ++i) {
// If one of the possibilities is the current tool
if (possibles[i] == current_tool) {
// We select the next tool in the possibilities
select_this_tool = possibles[(i + 1) % possibles.size()];
break;
}
}
}
}
ToolBar::instance()->selectTool(select_this_tool);
return true;
}
case KeyType::Command: {
Command* command = key->command();
// Commands are executed only when the main window is
// the current window running.
if (getForegroundWindow() == app->mainWindow()) {
// OK, so we can execute the command represented
// by the pressed-key in the message...
UIContext::instance()->executeCommandFromMenuOrShortcut(command, key->params());
return true;
}
break;
}
case KeyType::Quicktool: {
// Do nothing, it is used in the editor through the
// KeyboardShortcuts::getCurrentQuicktool() function.
break;
}
}
return false;
}

View File

@ -10,6 +10,6 @@
// Increment this value if the scripting API is modified between two
// released Aseprite versions.
#define API_VERSION 34
#define API_VERSION 35
#endif

View File

@ -34,10 +34,10 @@
#include "app/tools/tool_loop_manager.h"
#include "app/tx.h"
#include "app/ui/context_bar.h"
#include "app/ui/doc_view.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/tool_loop_impl.h"
#include "app/ui/main_window.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui_context.h"
#include "base/fs.h"
@ -498,6 +498,44 @@ int App_useTool(lua_State* L)
return 0;
}
int App_tip(lua_State* L)
{
const auto* ctx = App::instance()->context();
if (!ctx || !ctx->isUIAvailable() || !StatusBar::instance())
return 0; // No UI to show the tooltip
std::string text;
double duration = 2.0;
if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "text");
if (type == LUA_TSTRING)
text = lua_tostring(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 1, "duration");
if (type == LUA_TNUMBER)
duration = lua_tonumber(L, -1);
lua_pop(L, 1);
}
else {
if (!lua_isstring(L, 1))
return luaL_error(L, "app.tip text parameter must be a string");
text = lua_tostring(L, 1);
if (lua_isnumber(L, 2))
duration = lua_tonumber(L, 2);
}
if (text.empty())
return luaL_error(L, "app.tip text cannot be empty");
int msecs = std::clamp<int>(duration * 1000.0, 500, 30000);
StatusBar::instance()->showTip(msecs, text);
return 0;
}
int App_get_events(lua_State* L)
{
push_app_events(L);
@ -820,6 +858,7 @@ const luaL_Reg App_methods[] = {
{ "alert", App_alert },
{ "refresh", App_refresh },
{ "useTool", App_useTool },
{ "tip", App_tip },
{ nullptr, nullptr }
};

View File

@ -1,11 +1,12 @@
// 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.
#include "app/script/canvas_widget.h"
#include "app/color_spaces.h"
#include "app/script/graphics_context.h"
#include "app/ui/skin/skin_theme.h"
#include "os/system.h"
@ -45,7 +46,7 @@ void Canvas::callPaint()
return;
os::Paint p;
p.color(bgColor());
p.color(bgColor(), m_surface->colorSpace().get());
m_surface->drawRect(m_surface->bounds(), p);
// Draw only on resize (onPaint we draw the cached m_surface)
@ -189,7 +190,9 @@ void Canvas::onResize(ui::ResizeEvent& ev)
}
if (!m_surface || m_surface->width() != w || m_surface->height() != h) {
m_surface = system->makeSurface(w, h);
ui::Display* display = this->display();
os::ColorSpaceRef cs = (display ? display->colorSpace() : nullptr);
m_surface = system->makeSurface(w, h, cs);
callPaint();
}
}

View File

@ -60,6 +60,8 @@ namespace app { namespace script {
using namespace ui;
static constexpr const int kDefaultAutofit = ui::LEFT | ui::TOP;
namespace {
class DialogWindow : public WindowWithHand {
@ -107,6 +109,7 @@ struct Dialog {
std::map<std::string, ui::Widget*> dataWidgets;
std::map<std::string, ui::Widget*> labelWidgets;
int currentRadioGroup = 0;
int autofit = kDefaultAutofit;
// Member used to hold current state about the creation of a tabs
// widget. After creation it is reset to null to be ready for the
@ -193,12 +196,20 @@ struct Dialog {
it->second->setText(text);
}
void setAutofit(int align)
{
// Accept both 0 or a valid subset of align parameters.
if (align == 0 || (align & (ui::LEFT | ui::RIGHT | ui::TOP | ui::BOTTOM)))
autofit = align;
}
Display* parentDisplay() const
{
Display* parentDisplay = window.parentDisplay();
if (!parentDisplay) {
const auto mainWindow = App::instance()->mainWindow();
parentDisplay = mainWindow->display();
const auto* mainWindow = App::instance()->mainWindow();
if (mainWindow)
parentDisplay = mainWindow->display();
}
return parentDisplay;
}
@ -210,6 +221,9 @@ struct Dialog {
// origin/scale (or main window if a parent window wasn't specified).
if (window.ownDisplay()) {
const Display* parentDisplay = this->parentDisplay();
if (!parentDisplay)
return bounds;
const int scale = parentDisplay->scale();
const gfx::Point dialogOrigin = window.display()->nativeWindow()->contentRect().origin();
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
@ -224,6 +238,9 @@ struct Dialog {
window.expandWindow(rc.size());
Display* parentDisplay = this->parentDisplay();
if (!parentDisplay)
return;
const int scale = parentDisplay->scale();
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
gfx::Rect frame = window.display()->nativeWindow()->contentRect();
@ -231,8 +248,10 @@ struct Dialog {
window.display()->nativeWindow()->setFrame(frame);
}
else {
gfx::Rect oldBounds(window.bounds());
window.setBounds(rc);
window.invalidate();
parentDisplay()->invalidateRect(oldBounds);
}
}
@ -367,6 +386,7 @@ int Dialog_new(lua_State* L)
ui::Window::Type windowType = ui::Window::WithTitleBar;
std::string title = "Script";
bool sizeable = true;
int autofit = kDefaultAutofit;
if (lua_isstring(L, 1)) {
title = lua_tostring(L, 1);
}
@ -385,9 +405,16 @@ int Dialog_new(lua_State* L)
if (type != LUA_TNIL && !lua_toboolean(L, -1))
sizeable = false;
lua_pop(L, 1);
type = lua_getfield(L, 1, "autofit");
if (type != LUA_TNIL) {
autofit = lua_tointeger(L, -1);
}
lua_pop(L, 1);
}
auto dlg = push_new<Dialog>(L, windowType, title, sizeable);
dlg->setAutofit(autofit);
// The uservalue of the dialog userdata will contain a table that
// stores all the callbacks to handle events. As these callbacks can
@ -1516,6 +1543,10 @@ int Dialog_modify(lua_State* L)
type = lua_getfield(L, 2, "text");
if (const char* s = lua_tostring(L, -1)) {
widget->setText(s);
// Re-process mnemonics for buttons
if (widget->type() == WidgetType::kButtonWidget)
widget->processMnemonicFromText();
relayout = true;
}
lua_pop(L, 1);
@ -1633,6 +1664,10 @@ int Dialog_modify(lua_State* L)
lua_pop(L, 1);
type = lua_getfield(L, 2, "mouseCursor");
if (type == LUA_TNIL) {
lua_pop(L, 1);
type = lua_getfield(L, 2, "mousecursor");
}
if (type != LUA_TNIL) {
if (auto canvas = dynamic_cast<Canvas*>(widget)) {
auto cursor = (ui::CursorType)lua_tointeger(L, -1);
@ -1650,8 +1685,26 @@ int Dialog_modify(lua_State* L)
if (relayout && !dlg->window.isResizing()) {
dlg->window.layout();
gfx::Rect bounds(dlg->window.bounds().w, dlg->window.sizeHint().h);
dlg->window.expandWindow(bounds.size());
if (dlg->autofit > 0) {
gfx::Rect oldBounds = dlg->window.bounds();
gfx::Size resize(oldBounds.size());
if (dlg->autofit & ui::TOP || dlg->autofit & ui::BOTTOM)
resize.h = dlg->window.sizeHint().h;
if (dlg->autofit & ui::LEFT || dlg->autofit & ui::RIGHT)
resize.w = dlg->window.sizeHint().w;
gfx::Size difference = resize - oldBounds.size();
const auto& bounds = dlg->getWindowBounds();
gfx::Rect newBounds(bounds.x, bounds.y, resize.w, resize.h);
if (dlg->autofit & ui::BOTTOM)
newBounds.y = bounds.y - difference.h;
if (dlg->autofit & ui::RIGHT)
newBounds.x = bounds.x - difference.w;
dlg->setWindowBounds(newBounds);
}
}
}
lua_pushvalue(L, 1);
@ -1873,6 +1926,27 @@ int Dialog_get_bounds(lua_State* L)
return 1;
}
int Dialog_get_sizeHint(lua_State* L)
{
auto dlg = get_obj<Dialog>(L, 1);
push_new<gfx::Size>(L, dlg->window.sizeHint());
return 1;
}
int Dialog_get_autofit(lua_State* L)
{
auto dlg = get_obj<Dialog>(L, 1);
lua_pushinteger(L, dlg->autofit);
return 1;
}
int Dialog_set_autofit(lua_State* L)
{
auto dlg = get_obj<Dialog>(L, 1);
dlg->setAutofit(lua_tointeger(L, 2));
return 0;
}
int Dialog_set_bounds(lua_State* L)
{
auto dlg = get_obj<Dialog>(L, 1);
@ -1914,9 +1988,11 @@ const luaL_Reg Dialog_methods[] = {
};
const Property Dialog_properties[] = {
{ "data", Dialog_get_data, Dialog_set_data },
{ "bounds", Dialog_get_bounds, Dialog_set_bounds },
{ nullptr, nullptr, nullptr }
{ "data", Dialog_get_data, Dialog_set_data },
{ "bounds", Dialog_get_bounds, Dialog_set_bounds },
{ "autofit", Dialog_get_autofit, Dialog_set_autofit },
{ "sizeHint", Dialog_get_sizeHint, nullptr },
{ nullptr, nullptr, nullptr }
};
} // anonymous namespace

View File

@ -452,6 +452,7 @@ Engine::Engine() : L(luaL_newstate()), m_delegate(nullptr), m_printLastResult(fa
lua_setglobal(L, "FlipType");
setfield_integer(L, "HORIZONTAL", doc::algorithm::FlipType::FlipHorizontal);
setfield_integer(L, "VERTICAL", doc::algorithm::FlipType::FlipVertical);
setfield_integer(L, "DIAGONAL", doc::algorithm::FlipType::FlipDiagonal);
lua_pop(L, 1);
lua_newtable(L);

View File

@ -22,6 +22,7 @@
#include "app/script/luacpp.h"
#include "app/script/userdata.h"
#include "app/tx.h"
#include "app/ui/timeline/timeline.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
@ -355,6 +356,9 @@ int Layer_set_isCollapsed(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
layer->setCollapsed(lua_toboolean(L, 2));
if (auto* timeline = App::instance()->timeline())
timeline->refresh();
return 0;
}
@ -362,6 +366,9 @@ int Layer_set_isExpanded(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
layer->setCollapsed(!lua_toboolean(L, 2));
if (auto* timeline = App::instance()->timeline())
timeline->refresh();
return 0;
}

View File

@ -29,11 +29,16 @@ struct Plugin {
class PluginCommand : public Command {
public:
PluginCommand(const std::string& id, const std::string& title, int onclickRef, int onenabledRef)
PluginCommand(const std::string& id,
const std::string& title,
int onclickRef,
int onenabledRef,
int oncheckedRef)
: Command(id.c_str(), CmdUIOnlyFlag)
, m_title(title)
, m_onclickRef(onclickRef)
, m_onenabledRef(onenabledRef)
, m_oncheckedRef(oncheckedRef)
{
}
@ -72,28 +77,42 @@ protected:
bool onEnabled(Context* context) override
{
if (m_onenabledRef) {
script::Engine* engine = App::instance()->scriptEngine();
lua_State* L = engine->luaState();
lua_rawgeti(L, LUA_REGISTRYINDEX, m_onenabledRef);
if (lua_pcall(L, 0, 1, 0)) {
if (const char* s = lua_tostring(L, -1)) {
Console().printf("Error: %s", s);
return false;
}
}
else {
bool ret = lua_toboolean(L, -1);
lua_pop(L, 1);
return ret;
}
return callScriptRef(m_onenabledRef);
}
return true;
}
bool onChecked(Context* context) override
{
if (m_oncheckedRef) {
return callScriptRef(m_oncheckedRef);
}
return false;
}
private:
bool callScriptRef(int ref)
{
ASSERT(ref);
script::Engine* engine = App::instance()->scriptEngine();
lua_State* L = engine->luaState();
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
if (lua_pcall(L, 0, 1, 0)) {
if (const char* s = lua_tostring(L, -1))
Console().printf("Error: %s", s);
return false;
}
bool ret = lua_toboolean(L, -1);
lua_pop(L, 1);
return ret;
}
std::string m_title;
int m_onclickRef;
int m_onenabledRef;
int m_oncheckedRef;
};
void deleteCommandIfExistent(Extension* ext, const std::string& id)
@ -126,6 +145,7 @@ int Plugin_newCommand(lua_State* L)
if (lua_istable(L, 2)) {
std::string id, title, group;
int onenabledRef = 0;
int oncheckedRef = 0;
lua_getfield(L, 2, "id");
if (const char* s = lua_tostring(L, -1)) {
@ -156,6 +176,14 @@ int Plugin_newCommand(lua_State* L)
lua_pop(L, 1);
}
type = lua_getfield(L, 2, "onchecked");
if (type == LUA_TFUNCTION) {
oncheckedRef = luaL_ref(L, LUA_REGISTRYINDEX); // does a pop
}
else {
lua_pop(L, 1);
}
type = lua_getfield(L, 2, "onclick");
if (type == LUA_TFUNCTION) {
int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);
@ -164,7 +192,7 @@ int Plugin_newCommand(lua_State* L)
// overwriting a previous registered command)
deleteCommandIfExistent(plugin->ext, id);
auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef);
auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef, oncheckedRef);
Commands::instance()->add(cmd);
plugin->ext->addCommand(id);
@ -172,6 +200,7 @@ int Plugin_newCommand(lua_State* L)
if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
if (auto appMenus = AppMenus::instance()) {
auto menuItem = std::make_unique<AppMenuItem>(title, id);
menuItem->processMnemonicFromText();
appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
}
}

View File

@ -457,7 +457,7 @@ int Sprite_newCel(lua_State* L)
auto sprite = get_docobj<Sprite>(L, 1);
auto layerBase = get_docobj<Layer>(L, 2);
if (!layerBase->isImage())
return luaL_error(L, "unexpected kinf of layer in Sprite:newCel()");
return luaL_error(L, "unexpected kind of layer in Sprite:newCel()");
frame_t frame = get_frame_number_from_arg(L, 3);
if (frame < 0 || frame > sprite->lastFrame())

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

View File

@ -10,6 +10,7 @@
#include "config.h"
#endif
#include "app/color_spaces.h"
#include "app/util/conversion_to_surface.h"
#include "doc/blend_mode.h"
#include "doc/cel.h"
@ -21,7 +22,8 @@
namespace app { namespace thumb {
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
os::SurfaceRef get_cel_thumbnail(ui::Display* display,
const doc::Cel* cel,
const bool scaleUpToFit,
const gfx::Size& fitInSize)
{
@ -55,7 +57,8 @@ os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
if (os::SurfaceRef thumbnail = os::System::instance()->makeRgbaSurface(
thumbnailImage->width(),
thumbnailImage->height())) {
thumbnailImage->height(),
get_current_color_space(display))) {
convert_image_to_surface(thumbnailImage.get(),
palette,
thumbnail.get(),

View File

@ -11,6 +11,7 @@
#include "gfx/size.h"
#include "os/surface.h"
#include "ui/display.h"
namespace doc {
class Cel;
@ -22,7 +23,8 @@ class Surface;
namespace app { namespace thumb {
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
os::SurfaceRef get_cel_thumbnail(ui::Display* display,
const doc::Cel* cel,
const bool scaleUpToFit,
const gfx::Size& fitInSize);

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-2018 David Capello
//
// This program is distributed under the terms of
@ -107,11 +107,11 @@ public:
}
}
os::Surface* getCanvas(int w, int h, gfx::Color bgColor)
os::Surface* getCanvas(Display* display, int w, int h, gfx::Color bgColor)
{
assert_ui_thread();
auto activeCS = get_current_color_space();
auto activeCS = get_current_color_space(display);
if (!m_canvas || m_canvas->width() != w || m_canvas->height() != h ||
m_canvas->colorSpace() != activeCS) {
@ -432,9 +432,9 @@ void ColorSelector::onPaint(ui::PaintEvent& ev)
SkCanvas* canvas;
bool isSRGB;
// TODO compare both color spaces
if ((!get_current_color_space() || get_current_color_space()->isSRGB()) &&
(!g->getInternalSurface()->colorSpace() ||
g->getInternalSurface()->colorSpace()->isSRGB())) {
auto displayCs = get_current_color_space(display());
auto gCs = g->getInternalSurface()->colorSpace();
if ((!displayCs || displayCs->isSRGB()) && (!gCs || gCs->isSRGB())) {
// We can render directly in the ui::Graphics surface
canvas = &static_cast<os::SkiaSurface*>(g->getInternalSurface())->canvas();
isSRGB = true;
@ -442,7 +442,7 @@ void ColorSelector::onPaint(ui::PaintEvent& ev)
else {
// We'll paint in the ColorSelector::Painter canvas, and so we
// can convert color spaces.
painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
painterSurface = painter.getCanvas(display(), rc.w, rc.h, theme->colors.workspace());
canvas = &static_cast<os::SkiaSurface*>(painterSurface)->canvas();
isSRGB = false;
}
@ -497,7 +497,7 @@ void ColorSelector::onPaint(ui::PaintEvent& ev)
else
#endif // SK_ENABLE_SKSL
{
painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
painterSurface = painter.getCanvas(display(), rc.w, rc.h, theme->colors.workspace());
}
if (painterSurface)

View File

@ -58,9 +58,7 @@ public:
return;
}
// Color space conversion
auto convertColor = convert_from_current_to_screen_color_space();
Paint paint;
gfx::Color color = gfx::ColorNone;
int w = std::max(rc.w - 1, 1);
@ -110,7 +108,11 @@ public:
color = color_utils::color_for_ui(app::Color::fromGray(255 * x / w));
break;
}
g->drawVLine(convertColor(color), rc.x + x, rc.y, rc.h);
// Color space conversion
paint.color(color, get_current_color_space(slider->display()).get());
g->drawVLine(rc.x + x, rc.y, rc.h, paint);
}
}

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) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -11,6 +11,7 @@
#include "app/ui/color_wheel.h"
#include "app/color_spaces.h"
#include "app/color_utils.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
@ -358,6 +359,9 @@ void ColorWheel::onPaintMainArea(ui::Graphics* g, const gfx::Rect& rc)
int n = getHarmonies();
int boxsize = std::min(rc.w / 10, rc.h / 10);
ui::Paint paint;
auto cs = get_current_color_space(g->display());
for (int i = 0; i < n; ++i) {
app::Color color = getColorInHarmony(i);
double angle = color.getHsvHue() - 30.0;
@ -374,9 +378,10 @@ void ColorWheel::onPaintMainArea(ui::Graphics* g, const gfx::Rect& rc)
paintColorIndicator(g, pos, color.getHsvValue() < 0.5);
g->fillRect(
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), 255),
gfx::Rect(rc.x + rc.w - (n - i) * boxsize, rc.y + rc.h - boxsize, boxsize, boxsize));
paint.color(gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), 255), cs.get());
g->drawRect(
gfx::Rect(rc.x + rc.w - (n - i) * boxsize, rc.y + rc.h - boxsize, boxsize, boxsize),
paint);
}
}
}

View File

@ -1001,12 +1001,12 @@ public:
m_angle.setSuffix("°");
m_skew.setSuffix("°");
addChild(new Label("P:"));
addChild(new Label(Strings::context_bar_position_label()));
addChild(&m_x);
addChild(&m_y);
addChild(&m_w);
addChild(&m_h);
addChild(new Label("R:"));
addChild(new Label(Strings::context_bar_rotation_label()));
addChild(&m_angle);
addChild(&m_skew);
@ -1047,6 +1047,16 @@ public:
m_skew.Change.connect([this] { onChangeSkew(); });
}
void setupTooltips(TooltipManager* tooltipManager)
{
tooltipManager->addTooltipFor(&m_x, Strings::context_bar_position_x(), BOTTOM);
tooltipManager->addTooltipFor(&m_y, Strings::context_bar_position_y(), BOTTOM);
tooltipManager->addTooltipFor(&m_w, Strings::context_bar_size_width(), BOTTOM);
tooltipManager->addTooltipFor(&m_h, Strings::context_bar_size_height(), BOTTOM);
tooltipManager->addTooltipFor(&m_angle, Strings::context_bar_rotation_angle(), BOTTOM);
tooltipManager->addTooltipFor(&m_skew, Strings::context_bar_rotation_skew(), BOTTOM);
}
void update(const Transformation& t)
{
auto rc = t.bounds();
@ -1380,11 +1390,12 @@ public:
class ContextBar::DropPixelsField : public ButtonSet {
public:
DropPixelsField() : ButtonSet(2)
DropPixelsField() : ButtonSet(3)
{
auto* theme = SkinTheme::get(this);
addItem(theme->parts.dropPixelsOk(), theme->styles.contextBarButton());
addItem(theme->parts.dropPixelsDrop(), theme->styles.contextBarButton());
addItem(theme->parts.dropPixelsCancel(), theme->styles.contextBarButton());
setOfferCapture(false);
}
@ -1392,8 +1403,26 @@ public:
void setupTooltips(TooltipManager* tooltipManager)
{
// TODO Enter and Esc should be configurable keys
tooltipManager->addTooltipFor(at(0), Strings::context_bar_drop_pixel(), BOTTOM);
tooltipManager->addTooltipFor(at(1), Strings::context_bar_cancel_drag(), BOTTOM);
tooltipManager->addTooltipFor(
at(0),
key_tooltip(Strings::context_bar_drop_pixel_and_deselect().c_str(),
CommandId::DeselectMask(),
{},
KeyContext::Transformation),
BOTTOM);
tooltipManager->addTooltipFor(at(1),
key_tooltip(Strings::context_bar_drop_pixel().c_str(),
CommandId::Apply(),
{},
KeyContext::Transformation),
BOTTOM);
tooltipManager->addTooltipFor(at(2),
key_tooltip(Strings::context_bar_cancel_drag().c_str(),
CommandId::Undo(),
{},
KeyContext::Transformation),
BOTTOM);
}
obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
@ -1404,8 +1433,9 @@ protected:
ButtonSet::onItemChange(item);
switch (selectedItem()) {
case 0: DropPixels(ContextBarObserver::DropPixels); break;
case 1: DropPixels(ContextBarObserver::CancelDrag); break;
case 0: DropPixels(ContextBarObserver::Deselect); break;
case 1: DropPixels(ContextBarObserver::DropPixels); break;
case 2: DropPixels(ContextBarObserver::CancelDrag); break;
}
}
};
@ -2626,6 +2656,7 @@ void ContextBar::setupTooltips(TooltipManager* tooltipManager)
m_dropPixels->setupTooltips(tooltipManager);
m_symmetry->setupTooltips(tooltipManager);
m_sliceFields->setupTooltips(tooltipManager);
m_transformation->setupTooltips(tooltipManager);
}
void ContextBar::registerCommands()

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -12,7 +13,7 @@ namespace app {
class ContextBarObserver {
public:
enum DropAction { DropPixels, CancelDrag };
enum DropAction { Deselect, DropPixels, CancelDrag };
virtual ~ContextBarObserver() {}
virtual void onDropPixels(DropAction action) {}

View File

@ -36,12 +36,15 @@
#include "app/ui/workspace.h"
#include "app/ui_context.h"
#include "app/util/clipboard.h"
#include "app/util/slice_utils.h"
#include "base/fs.h"
#include "doc/color.h"
#include "doc/layer.h"
#include "doc/slice.h"
#include "doc/sprite.h"
#include "fmt/format.h"
#include "ui/alert.h"
#include "ui/display.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/shortcut.h"
@ -306,6 +309,11 @@ bool DocView::onCloseView(Workspace* workspace, bool quitting)
// See if the sprite has changes
while (m_document->isModified()) {
if (quitting) {
// Make sure the window is active so we can see the message when we close the app.
display()->nativeWindow()->activate();
}
// ask what want to do the user with the changes in the sprite
int ret = Alert::show(Strings::alerts_save_sprite_changes(
m_document->name(),
@ -510,6 +518,8 @@ bool DocView::onCanCopy(Context* ctx)
return true;
else if (m_editor->isMovingPixels())
return true;
else if (m_editor->hasSelectedSlices())
return true;
else
return false;
}
@ -528,6 +538,11 @@ bool DocView::onCanPaste(Context* ctx)
return true;
}
}
if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable) &&
ctx->clipboard()->format() == ClipboardFormat::Slices) {
return true;
}
return false;
}
@ -560,15 +575,22 @@ bool DocView::onCopy(Context* ctx)
ctx->clipboard()->copy(reader);
return true;
}
else
return false;
std::vector<Slice*> selectedSlices = get_selected_slices(reader.site());
if (!selectedSlices.empty()) {
ctx->clipboard()->copySlices(selectedSlices);
return true;
}
return false;
}
bool DocView::onPaste(Context* ctx, const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::Image ||
clipboard->format() == ClipboardFormat::Tilemap) {
clipboard->format() == ClipboardFormat::Tilemap ||
clipboard->format() == ClipboardFormat::Slices) {
clipboard->paste(ctx, true, position);
return true;
}

View File

@ -2555,6 +2555,16 @@ void Editor::onBeforeLayerEditableChange(DocEvent& ev, bool newState)
m_state->onBeforeLayerEditableChange(this, ev.layer(), newState);
}
void Editor::onBeforeSlicesDuplication(DocEvent& ev)
{
clearSlicesSelection();
}
void Editor::onSliceDuplicated(DocEvent& ev)
{
selectSlice(ev.slice());
}
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
{
bool used = false;

View File

@ -343,6 +343,8 @@ protected:
void onRemoveSlice(DocEvent& ev) override;
void onBeforeLayerVisibilityChange(DocEvent& ev, bool newState) override;
void onBeforeLayerEditableChange(DocEvent& ev, bool newState) override;
void onBeforeSlicesDuplication(DocEvent& ev) override;
void onSliceDuplicated(DocEvent& ev) override;
// ActiveToolObserver impl
void onActiveToolChange(tools::Tool* tool) override;

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
@ -22,6 +22,7 @@
#include "app/commands/commands.h"
#include "app/commands/move_thing.h"
#include "app/console.h"
#include "app/i18n/strings.h"
#include "app/modules/gui.h"
#include "app/pref/preferences.h"
#include "app/tools/ink.h"
@ -48,6 +49,7 @@
#include "fmt/format.h"
#include "gfx/rect.h"
#include "ui/manager.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/system.h"
#include "ui/view.h"
@ -286,9 +288,6 @@ bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
UIContext* ctx = UIContext::instance();
ctx->setActiveView(editor->getDocView());
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(getTransformation(editor));
// Start scroll loop
if (editor->checkForScroll(msg) || editor->checkForZoom(msg))
return true;
@ -442,10 +441,6 @@ void MovingPixelsState::onCommitMouseMove(Editor* editor, const gfx::PointF& spr
// Drag the image to that position
m_pixelsMovement->moveImage(spritePos, moveModifier);
// Update context bar and status bar
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(transformation);
m_editor->updateStatusBar();
}
@ -475,19 +470,6 @@ bool MovingPixelsState::onKeyDown(Editor* editor, KeyMessage* msg)
// FineControl now (e.g. if we pressed another modifier key).
m_lockedKeyAction = KeyAction::None;
if (msg->scancode() == kKeyEnter || // TODO make this key customizable
msg->scancode() == kKeyEnterPad || msg->scancode() == kKeyEsc) {
dropPixels();
// The escape key drop pixels and deselect the mask.
if (msg->scancode() == kKeyEsc) { // TODO make this key customizable
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
}
return true;
}
// Use StandbyState implementation
return StandbyState::onKeyDown(editor, msg);
}
@ -529,6 +511,10 @@ bool MovingPixelsState::onUpdateStatusBar(Editor* editor)
const Transformation& transform(getTransformation(editor));
gfx::Size imageSize = m_pixelsMovement->getInitialImageSize();
// Update the context bar along with the status bar
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(transform);
int w = int(transform.bounds().w);
int h = int(transform.bounds().h);
int gcd = base::gcd(w, h);
@ -634,6 +620,12 @@ void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev)
return;
}
}
// Handle undo directly as cancelDrag() to avoid adding an action in the history.
else if (command->id() == CommandId::Undo()) {
cancelDrag();
ev.cancel();
return;
}
// Don't drop pixels if the user zooms/scrolls/picks a color
// using commands.
else if ((command->id() == CommandId::Zoom()) || (command->id() == CommandId::Scroll()) ||
@ -795,18 +787,35 @@ void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action)
return;
switch (action) {
case ContextBarObserver::Deselect: deselect(); break;
case ContextBarObserver::DropPixels: dropPixels(); break;
case ContextBarObserver::CancelDrag:
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
m_discarded = true;
// Quit from MovingPixelsState, back to standby.
m_editor->backToPreviousState();
break;
case ContextBarObserver::CancelDrag: cancelDrag(); break;
}
}
void MovingPixelsState::deselect()
{
if (!m_pixelsMovement || m_discarded)
return;
dropPixels();
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
}
void MovingPixelsState::cancelDrag()
{
if (!m_pixelsMovement || m_discarded)
return;
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
m_discarded = true;
// Quit from MovingPixelsState, back to standby.
dropPixels();
}
void MovingPixelsState::onPivotChange()
{
ContextBar* contextBar = App::instance()->contextBar();

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
@ -94,7 +94,10 @@ private:
void onBeforeCommandExecution(CommandExecutionEvent& ev);
void setTransparentColor(bool opaque, const app::Color& color);
void deselect();
void dropPixels();
void cancelDrag();
bool isActiveDocument() const;
bool isActiveEditor() const;

View File

@ -74,7 +74,11 @@ bool StateWithWheelBehavior::onMouseWheel(Editor* editor, MouseMessage* msg)
double dz = delta.x + delta.y;
WheelAction wheelAction = WheelAction::None;
if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
if (tools::Tool* quickTool = App::instance()->activeToolManager()->quickTool();
quickTool && quickTool->getId() == tools::WellKnownInks::Zoom) {
wheelAction = WheelAction::Zoom;
}
else if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
if (!Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel())
wheelAction = WheelAction::VScroll;
else

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
@ -45,6 +45,7 @@
#include "app/ui_context.h"
#include "app/util/expand_cel_canvas.h"
#include "app/util/layer_utils.h"
#include "app/util/slice_utils.h"
#include "doc/brush.h"
#include "doc/cel.h"
#include "doc/image.h"
@ -693,7 +694,7 @@ public:
// popup menu to create a new one.
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
Slice* slice = new Slice;
slice->setName(getUniqueSliceName());
slice->setName(get_unique_slice_name(m_sprite));
SliceKey key(bounds);
slice->insert(getFrame(), key);
@ -716,18 +717,6 @@ private:
// EditorObserver impl
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
std::string getUniqueSliceName() const
{
std::string prefix = "Slice";
int max = 0;
for (Slice* slice : m_sprite->slices())
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
return fmt::format("{} {}", prefix, max + 1);
}
};
//////////////////////////////////////////////////////////////////////

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) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -209,7 +209,8 @@ gfx::Rect TransformHandles::getPivotHandleBounds(Editor* editor,
{
auto theme = SkinTheme::get(editor);
gfx::Size partSize = theme->parts.pivotHandle()->size();
gfx::Point screenPivotPos = editor->editorToScreen(gfx::Point(transform.pivot()));
gfx::Point pivotPos = gfx::Point(transform.pivot()) + editor->mainTilePosition();
gfx::Point screenPivotPos = editor->editorToScreen(pivotPos);
screenPivotPos.x += editor->projection().applyX(1) / 2;
screenPivotPos.y += editor->projection().applyY(1) / 2;

View File

@ -20,9 +20,11 @@
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "fmt/format.h"
#include "text/font_style_set.h"
#include "ui/display.h"
#include "ui/fit_bounds.h"
#include "ui/manager.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/scale.h"
@ -271,7 +273,7 @@ FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, tru
addItem("...");
setMultiMode(MultiMode::Set);
tooltips->addTooltipFor(getItem(0), Strings::text_tool_bold(), BOTTOM);
tooltips->addTooltipFor(getItem(0), Strings::font_style_font_weight(), BOTTOM);
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
}
@ -384,17 +386,72 @@ void FontEntry::setInfo(const FontInfo& info, const From fromField)
{
m_info = info;
if (fromField != From::Face)
m_face.setText(info.title());
auto family = theme()->fontMgr()->matchFamily(m_info.name());
bool hasBold = false;
m_availableWeights.clear();
if (family) {
auto checkWeight = [this, &family, &hasBold](text::FontStyle::Weight w) {
auto ref = family->matchStyle(
text::FontStyle(w, m_info.style().width(), m_info.style().slant()));
if (ref->fontStyle().weight() == w)
m_availableWeights.push_back(w);
if (ref->fontStyle().weight() == text::FontStyle::Weight::Bold)
hasBold = true;
};
checkWeight(text::FontStyle::Weight::Thin);
checkWeight(text::FontStyle::Weight::ExtraLight);
checkWeight(text::FontStyle::Weight::Light);
checkWeight(text::FontStyle::Weight::Normal);
checkWeight(text::FontStyle::Weight::Medium);
checkWeight(text::FontStyle::Weight::SemiBold);
checkWeight(text::FontStyle::Weight::Bold);
checkWeight(text::FontStyle::Weight::Black);
checkWeight(text::FontStyle::Weight::ExtraBlack);
}
else {
// Stick to only "normal" for fonts without a family.
m_availableWeights.push_back(text::FontStyle::Weight::Normal);
m_style.getItem(0)->setEnabled(false);
}
if (std::find(m_availableWeights.begin(), m_availableWeights.end(), m_info.style().weight()) ==
m_availableWeights.end()) {
// The currently selected weight is not available, reset it back to normal.
m_info = app::FontInfo(m_info,
m_info.size(),
text::FontStyle(text::FontStyle::Weight::Normal,
m_info.style().width(),
m_info.style().slant()),
m_info.flags(),
m_info.hinting());
}
if (fromField != From::Face) {
m_face.setText(m_info.title());
}
if (fromField != From::Size) {
m_size.updateForFont(info);
m_size.setValue(fmt::format("{}", info.size()));
m_size.updateForFont(m_info);
m_size.setValue(fmt::format("{}", m_info.size()));
}
m_style.getItem(0)->setEnabled(hasBold);
m_style.getItem(0)->setSelected(m_info.style().weight() != text::FontStyle::Weight::Normal);
m_style.getItem(0)->setText("B");
// Give some indication of what the weight is, if we have any variation
if (m_style.getItem(0)->isSelected() && m_availableWeights.size() > 1) {
if (m_info.style().weight() > text::FontStyle::Weight::Bold)
m_style.getItem(0)->setText("B+");
else if (m_info.style().weight() < text::FontStyle::Weight::Bold)
m_style.getItem(0)->setText("B-");
}
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);
m_style.getItem(1)->setSelected(m_info.style().slant() != text::FontStyle::Slant::Upright);
}
FontChange(m_info, fromField);
@ -430,17 +487,51 @@ void FontEntry::onStyleItemClick(ButtonSet::Item* item)
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());
if (m_availableWeights.size() > 2) {
// Ensure consistency, since the click also affects the "selected" highlighting.
item->setSelected(style.weight() != text::FontStyle::Weight::Normal);
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
From::Style);
Menu weightMenu;
auto currentWeight = m_info.style().weight();
auto weightChange = [this](text::FontStyle::Weight newWeight) {
const text::FontStyle style(newWeight, m_info.style().width(), m_info.style().slant());
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
From::Style);
};
for (auto weight : m_availableWeights) {
auto* menuItem = new MenuItem(Strings::Translate(
fmt::format("font_style.font_weight_{}", static_cast<int>(weight)).c_str()));
menuItem->setSelected(weight == currentWeight);
if (!menuItem->isSelected())
menuItem->Click.connect([&weightChange, weight] { weightChange(weight); });
if (weight == text::FontStyle::Weight::Bold &&
currentWeight == text::FontStyle::Weight::Normal)
menuItem->setHighlighted(true);
weightMenu.addChild(menuItem);
}
weightMenu.initTheme();
const auto& bounds = m_style.getItem(0)->bounds();
weightMenu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
}
else {
const bool isBold = m_info.style().weight() == text::FontStyle::Weight::Bold;
style = text::FontStyle(
isBold ? text::FontStyle::Weight::Normal : text::FontStyle::Weight::Bold,
style.width(),
style.slant());
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
From::Style);
}
break;
}
// Italic button changed
// Italic button changed
case 1: {
const bool italic = m_style.getItem(1)->isSelected();
style = text::FontStyle(

View File

@ -115,6 +115,7 @@ private:
FontSize m_size;
FontStyle m_style;
std::unique_ptr<FontStroke> m_stroke;
std::vector<text::FontStyle::Weight> m_availableWeights;
bool m_lockFace = false;
};

647
src/app/ui/key.cpp Normal file
View File

@ -0,0 +1,647 @@
// Aseprite
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 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/ui/key.h"
#include "app/commands/command.h"
#include "app/i18n/strings.h"
#include "app/tools/tool.h"
#include "app/ui/keyboard_shortcuts.h"
#include "ui/message.h"
#include "ui/shortcut.h"
#include <string>
#include <vector>
#define I18N_KEY(a) app::Strings::keyboard_shortcuts_##a()
namespace {
struct KeyShortcutAction {
const char* name;
std::string userfriendly;
app::KeyAction action;
app::KeyContext context;
};
std::vector<KeyShortcutAction> g_actions;
const std::vector<KeyShortcutAction>& actions()
{
if (g_actions.empty()) {
g_actions = std::vector<KeyShortcutAction>{
{ "CopySelection",
I18N_KEY(copy_selection),
app::KeyAction::CopySelection,
app::KeyContext::TranslatingSelection },
{ "SnapToGrid",
I18N_KEY(snap_to_grid),
app::KeyAction::SnapToGrid,
app::KeyContext::TranslatingSelection },
{ "LockAxis",
I18N_KEY(lock_axis),
app::KeyAction::LockAxis,
app::KeyContext::TranslatingSelection },
{ "FineControl",
I18N_KEY(fine_translating),
app::KeyAction::FineControl,
app::KeyContext::TranslatingSelection },
{ "MaintainAspectRatio",
I18N_KEY(maintain_aspect_ratio),
app::KeyAction::MaintainAspectRatio,
app::KeyContext::ScalingSelection },
{ "ScaleFromCenter",
I18N_KEY(scale_from_center),
app::KeyAction::ScaleFromCenter,
app::KeyContext::ScalingSelection },
{ "FineControl",
I18N_KEY(fine_scaling),
app::KeyAction::FineControl,
app::KeyContext::ScalingSelection },
{ "AngleSnap",
I18N_KEY(angle_snap),
app::KeyAction::AngleSnap,
app::KeyContext::RotatingSelection },
{ "AddSelection",
I18N_KEY(add_selection),
app::KeyAction::AddSelection,
app::KeyContext::SelectionTool },
{ "SubtractSelection",
I18N_KEY(subtract_selection),
app::KeyAction::SubtractSelection,
app::KeyContext::SelectionTool },
{ "IntersectSelection",
I18N_KEY(intersect_selection),
app::KeyAction::IntersectSelection,
app::KeyContext::SelectionTool },
{ "AutoSelectLayer",
I18N_KEY(auto_select_layer),
app::KeyAction::AutoSelectLayer,
app::KeyContext::MoveTool },
{ "StraightLineFromLastPoint",
I18N_KEY(line_from_last_point),
app::KeyAction::StraightLineFromLastPoint,
app::KeyContext::FreehandTool },
{ "AngleSnapFromLastPoint",
I18N_KEY(angle_from_last_point),
app::KeyAction::AngleSnapFromLastPoint,
app::KeyContext::FreehandTool },
{ "MoveOrigin",
I18N_KEY(move_origin),
app::KeyAction::MoveOrigin,
app::KeyContext::ShapeTool },
{ "SquareAspect",
I18N_KEY(square_aspect),
app::KeyAction::SquareAspect,
app::KeyContext::ShapeTool },
{ "DrawFromCenter",
I18N_KEY(draw_from_center),
app::KeyAction::DrawFromCenter,
app::KeyContext::ShapeTool },
{ "RotateShape",
I18N_KEY(rotate_shape),
app::KeyAction::RotateShape,
app::KeyContext::ShapeTool },
{ "LeftMouseButton",
I18N_KEY(trigger_left_mouse_button),
app::KeyAction::LeftMouseButton,
app::KeyContext::Any },
{ "RightMouseButton",
I18N_KEY(trigger_right_mouse_button),
app::KeyAction::RightMouseButton,
app::KeyContext::Any }
};
}
return g_actions;
}
struct {
const char* name;
app::KeyContext context;
} g_contexts[] = {
{ "", app::KeyContext::Any },
{ "Normal", app::KeyContext::Normal },
{ "Selection", app::KeyContext::SelectionTool },
{ "TranslatingSelection", app::KeyContext::TranslatingSelection },
{ "ScalingSelection", app::KeyContext::ScalingSelection },
{ "RotatingSelection", app::KeyContext::RotatingSelection },
{ "MoveTool", app::KeyContext::MoveTool },
{ "FreehandTool", app::KeyContext::FreehandTool },
{ "ShapeTool", app::KeyContext::ShapeTool },
{ "FramesSelection", app::KeyContext::FramesSelection },
{ "Transformation", app::KeyContext::Transformation },
{ NULL, app::KeyContext::Any }
};
using Vec = app::DragVector;
struct KeyShortcutWheelAction {
const char* name;
const std::string userfriendly;
Vec vector;
};
std::vector<KeyShortcutWheelAction> g_wheel_actions;
const std::vector<KeyShortcutWheelAction>& wheel_actions()
{
if (g_wheel_actions.empty()) {
g_wheel_actions = std::vector<KeyShortcutWheelAction>{
{ "", "", Vec(0.0, 0.0) },
{ "Zoom", I18N_KEY(zoom), Vec(8.0, 0.0) },
{ "VScroll", I18N_KEY(scroll_vertically), Vec(4.0, 0.0) },
{ "HScroll", I18N_KEY(scroll_horizontally), Vec(4.0, 0.0) },
{ "FgColor", I18N_KEY(fg_color), Vec(8.0, 0.0) },
{ "BgColor", I18N_KEY(bg_color), Vec(8.0, 0.0) },
{ "FgTile", I18N_KEY(fg_tile), Vec(8.0, 0.0) },
{ "BgTile", I18N_KEY(bg_tile), Vec(8.0, 0.0) },
{ "Frame", I18N_KEY(change_frame), Vec(16.0, 0.0) },
{ "BrushSize", I18N_KEY(change_brush_size), Vec(4.0, 0.0) },
{ "BrushAngle", I18N_KEY(change_brush_angle), Vec(-4.0, 0.0) },
{ "ToolSameGroup", I18N_KEY(change_tool_same_group), Vec(8.0, 0.0) },
{ "ToolOtherGroup", I18N_KEY(change_tool), Vec(0.0, -8.0) },
{ "Layer", I18N_KEY(change_layer), Vec(0.0, 8.0) },
{ "InkType", I18N_KEY(change_ink_type), Vec(0.0, -16.0) },
{ "InkOpacity", I18N_KEY(change_ink_opacity), Vec(0.0, 1.0) },
{ "LayerOpacity", I18N_KEY(change_layer_opacity), Vec(0.0, 1.0) },
{ "CelOpacity", I18N_KEY(change_cel_opacity), Vec(0.0, 1.0) },
{ "Alpha", I18N_KEY(color_alpha), Vec(4.0, 0.0) },
{ "HslHue", I18N_KEY(color_hsl_hue), Vec(1.0, 0.0) },
{ "HslSaturation", I18N_KEY(color_hsl_saturation), Vec(4.0, 0.0) },
{ "HslLightness", I18N_KEY(color_hsl_lightness), Vec(0.0, 4.0) },
{ "HsvHue", I18N_KEY(color_hsv_hue), Vec(1.0, 0.0) },
{ "HsvSaturation", I18N_KEY(color_hsv_saturation), Vec(4.0, 0.0) },
{ "HsvValue", I18N_KEY(color_hsv_value), Vec(0.0, 4.0) }
};
}
return g_wheel_actions;
}
std::string get_user_friendly_string_for_keyaction(app::KeyAction action, app::KeyContext context)
{
for (const auto& a : actions()) {
if (action == a.action && context == a.context)
return a.userfriendly;
}
return {};
}
std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelAction)
{
const int c = int(wheelAction);
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last))
return wheel_actions()[c].userfriendly;
return {};
}
void erase_shortcut(app::AppShortcuts& kvs,
const app::KeySource source,
const ui::Shortcut& shortcut)
{
for (auto it = kvs.begin(); it != kvs.end();) {
auto& kv = *it;
if (kv.source() == source && kv.shortcut() == shortcut)
it = kvs.erase(it);
else
++it;
}
}
void erase_shortcuts(app::AppShortcuts& kvs, const app::KeySource source)
{
for (auto it = kvs.begin(); it != kvs.end();) {
auto& kv = *it;
if (kv.source() == source)
it = kvs.erase(it);
else
++it;
}
}
} // anonymous namespace
namespace base {
template<>
app::KeyAction convert_to(const std::string& from)
{
for (const auto& a : actions()) {
if (from == a.name)
return a.action;
}
return app::KeyAction::None;
}
template<>
std::string convert_to(const app::KeyAction& from)
{
for (const auto& a : actions()) {
if (from == a.action)
return a.name;
}
return {};
}
template<>
app::WheelAction convert_to(const std::string& from)
{
for (int c = int(app::WheelAction::First); c <= int(app::WheelAction::Last); ++c) {
if (from == wheel_actions()[c].name)
return (app::WheelAction)c;
}
return app::WheelAction::None;
}
template<>
std::string convert_to(const app::WheelAction& from)
{
const int c = int(from);
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last))
return wheel_actions()[c].name;
return {};
}
template<>
app::KeyContext convert_to(const std::string& from)
{
for (int c = 0; g_contexts[c].name; ++c) {
if (from == g_contexts[c].name)
return g_contexts[c].context;
}
return app::KeyContext::Any;
}
template<>
std::string convert_to(const app::KeyContext& from)
{
for (int c = 0; g_contexts[c].name; ++c) {
if (from == g_contexts[c].context)
return g_contexts[c].name;
}
return {};
}
} // namespace base
namespace app {
using namespace ui;
bool AppShortcut::fitsBetterThan(const KeyContext currentContext,
const KeyContext thisShortcutContext,
const KeyContext otherShortcutContext,
const AppShortcut& otherShortcut) const
{
// Better context in the same source
if (otherShortcut.source() == this->source() && otherShortcutContext != currentContext &&
thisShortcutContext == currentContext)
return true;
// Better key source/level: User-defined > Extension-defined > App-defined
if (int(source()) > int(otherShortcut.source()) && (thisShortcutContext == currentContext ||
// User-defined "Any" context overwrites all
// app-defined context
thisShortcutContext == KeyContext::Any))
return true;
// Normal > SelectionTool > Transformation
if ((currentContext == KeyContext::Transformation &&
otherShortcutContext != KeyContext::Transformation &&
thisShortcutContext == KeyContext::SelectionTool))
return true;
return false;
}
Key::Key(const Key& k)
: m_type(k.m_type)
, m_adds(k.m_adds)
, m_dels(k.m_dels)
, m_keycontext(k.m_keycontext)
{
switch (m_type) {
case KeyType::Command:
m_command = k.m_command;
m_params = k.m_params;
break;
case KeyType::Tool:
case KeyType::Quicktool: m_tool = k.m_tool; break;
case KeyType::Action: m_action = k.m_action; break;
case KeyType::WheelAction:
m_action = k.m_action;
m_wheelAction = k.m_wheelAction;
break;
case KeyType::DragAction:
m_action = k.m_action;
m_wheelAction = k.m_wheelAction;
m_dragVector = k.m_dragVector;
break;
}
}
Key::Key(Command* command, const Params& params, const KeyContext keyContext)
: m_type(KeyType::Command)
, m_keycontext(keyContext)
, m_command(command)
, m_params(params)
{
}
Key::Key(const KeyType type, tools::Tool* tool)
: m_type(type)
, m_keycontext(KeyContext::Any)
, m_tool(tool)
{
}
Key::Key(const KeyAction action, const KeyContext keyContext)
: m_type(KeyType::Action)
, m_keycontext(keyContext)
, m_action(action)
{
if (m_keycontext != KeyContext::Any)
return;
// Automatic key context
switch (action) {
case KeyAction::None: m_keycontext = KeyContext::Any; break;
case KeyAction::CopySelection:
case KeyAction::SnapToGrid:
case KeyAction::LockAxis:
case KeyAction::FineControl: m_keycontext = KeyContext::TranslatingSelection; break;
case KeyAction::AngleSnap: m_keycontext = KeyContext::RotatingSelection; break;
case KeyAction::MaintainAspectRatio:
case KeyAction::ScaleFromCenter: m_keycontext = KeyContext::ScalingSelection; break;
case KeyAction::AddSelection:
case KeyAction::SubtractSelection:
case KeyAction::IntersectSelection: m_keycontext = KeyContext::SelectionTool; break;
case KeyAction::AutoSelectLayer: m_keycontext = KeyContext::MoveTool; break;
case KeyAction::StraightLineFromLastPoint:
case KeyAction::AngleSnapFromLastPoint: m_keycontext = KeyContext::FreehandTool; break;
case KeyAction::MoveOrigin:
case KeyAction::SquareAspect:
case KeyAction::DrawFromCenter:
case KeyAction::RotateShape: m_keycontext = KeyContext::ShapeTool; break;
case KeyAction::LeftMouseButton:
case KeyAction::RightMouseButton: m_keycontext = KeyContext::Any; break;
}
}
Key::Key(const WheelAction wheelAction)
: m_type(KeyType::WheelAction)
, m_keycontext(KeyContext::MouseWheel)
, m_action(KeyAction::None)
, m_wheelAction(wheelAction)
{
}
// static
KeyPtr Key::MakeDragAction(WheelAction dragAction)
{
KeyPtr k(new Key(dragAction));
k->m_type = KeyType::DragAction;
k->m_keycontext = KeyContext::Any;
k->m_dragVector = wheel_actions()[(int)dragAction].vector;
return k;
}
const AppShortcuts& Key::shortcuts() const
{
if (!m_shortcuts) {
m_shortcuts = std::make_unique<AppShortcuts>();
// Add default keys
for (const auto& kv : m_adds) {
if (kv.source() == KeySource::Original)
m_shortcuts->add(kv);
}
// Delete/add extension-defined keys
for (const auto& kv : m_dels) {
if (kv.source() == KeySource::ExtensionDefined)
m_shortcuts->remove(kv);
else {
ASSERT(kv.source() != KeySource::Original);
}
}
for (const auto& kv : m_adds) {
if (kv.source() == KeySource::ExtensionDefined)
m_shortcuts->add(kv);
}
// Delete/add user-defined keys
for (const auto& kv : m_dels) {
if (kv.source() == KeySource::UserDefined)
m_shortcuts->remove(kv);
}
for (const auto& kv : m_adds) {
if (kv.source() == KeySource::UserDefined)
m_shortcuts->add(kv);
}
}
return *m_shortcuts;
}
void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShortcuts& globalKeys)
{
m_adds.push_back(AppShortcut(source, shortcut));
m_shortcuts.reset();
// Remove the shortcut from other commands
if (source == KeySource::ExtensionDefined || source == KeySource::UserDefined) {
erase_shortcut(m_dels, source, shortcut);
globalKeys.disableShortcut(shortcut, source, m_keycontext, this);
}
}
bool Key::fitsContext(const KeyContext keyContext) const
{
// This key is for any context
if (m_keycontext == KeyContext::Any)
return true;
// This key is for the same context
if (m_keycontext == keyContext)
return true;
// Use Normal or SelectionTool keys in Transformation context
if (keyContext == KeyContext::Transformation &&
(m_keycontext == KeyContext::SelectionTool || m_keycontext == KeyContext::Normal))
return true;
// Use Normal keys in SelectionTool or FramesSelection contexts
if ((keyContext == KeyContext::SelectionTool || keyContext == KeyContext::FramesSelection) &&
(m_keycontext == KeyContext::Normal))
return true;
return false;
}
const AppShortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
{
const AppShortcut* best = nullptr;
if (const auto* keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
if (fitsContext(keyContext)) {
for (const AppShortcut& shortcut : shortcuts()) {
if (shortcut.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
(!best || shortcut.fitsBetterThan(keyContext, keycontext(), keycontext(), *best))) {
best = &shortcut;
}
}
}
}
else if (const auto* mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
if (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) {
for (const AppShortcut& shortcut : shortcuts()) {
if (shortcut.modifiers() == mouseMsg->modifiers())
return &shortcut;
}
}
}
return best;
}
const AppShortcut* Key::isPressed(const Message* msg) const
{
return isPressed(msg, KeyboardShortcuts::getCurrentKeyContext());
}
bool Key::isPressed() const
{
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const AppShortcut& shortcut) {
return shortcut.isPressed();
});
}
bool Key::isLooselyPressed() const
{
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const AppShortcut& shortcut) {
return shortcut.isLooselyPressed();
});
}
bool Key::isCommandListed() const
{
return type() == KeyType::Command && command()->isListed(params());
}
bool Key::hasShortcut(const ui::Shortcut& shortcut) const
{
return shortcuts().has(AppShortcut(
// KeySource is not used in has()
KeySource::Original,
shortcut));
}
bool Key::hasUserDefinedShortcuts() const
{
return std::any_of(m_adds.begin(), m_adds.end(), [](const auto& kv) {
return (kv.source() == KeySource::UserDefined);
});
}
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 shortcut.
ASSERT(source != KeySource::Original);
erase_shortcut(m_adds, source, shortcut);
erase_shortcut(m_dels, source, shortcut);
m_dels.push_back(AppShortcut(source, shortcut));
m_shortcuts.reset();
}
void Key::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_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.push_back(AppShortcut(KeySource::UserDefined, kv));
m_shortcuts.reset();
}
std::string Key::triggerString() const
{
switch (m_type) {
case KeyType::Command: m_command->loadParams(m_params); return m_command->friendlyName();
case KeyType::Tool:
case KeyType::Quicktool: {
std::string text = m_tool->getText();
if (m_type == KeyType::Quicktool)
text += " (quick)";
return text;
}
case KeyType::Action: return get_user_friendly_string_for_keyaction(m_action, m_keycontext);
case KeyType::WheelAction:
case KeyType::DragAction: return get_user_friendly_string_for_wheelaction(m_wheelAction);
}
return "Unknown";
}
void reset_key_tables_that_depends_on_language()
{
g_actions.clear();
g_wheel_actions.clear();
}
std::string key_tooltip(const char* str, const app::Key* key)
{
std::string res;
if (str)
res += str;
if (key && !key->shortcuts().empty()) {
res += " (";
res += key->shortcuts().front().toString();
res += ")";
}
return res;
}
std::string convert_keycontext_to_user_friendly_string(const KeyContext keyctx)
{
switch (keyctx) {
case KeyContext::Any: return {};
case KeyContext::Normal: return I18N_KEY(key_context_normal);
case KeyContext::SelectionTool: return I18N_KEY(key_context_selection);
case KeyContext::TranslatingSelection: return I18N_KEY(key_context_translating_selection);
case KeyContext::ScalingSelection: return I18N_KEY(key_context_scaling_selection);
case KeyContext::RotatingSelection: return I18N_KEY(key_context_rotating_selection);
case KeyContext::MoveTool: return I18N_KEY(key_context_move_tool);
case KeyContext::FreehandTool: return I18N_KEY(key_context_freehand_tool);
case KeyContext::ShapeTool: return I18N_KEY(key_context_shape_tool);
case KeyContext::FramesSelection: return I18N_KEY(key_context_frames_selection);
case KeyContext::Transformation: return I18N_KEY(key_context_transformation);
}
return {};
}
} // namespace app

View File

@ -103,12 +103,42 @@ inline KeyAction operator&(KeyAction a, KeyAction b)
return KeyAction(int(a) & int(b));
}
// This is a ui::Shortcut wrapper (just one key shortcut) defined by
// the app, an extension, or the user (KeySource).
class AppShortcut : public ui::Shortcut {
public:
AppShortcut(const KeySource source, const ui::Shortcut& shortcut)
: Shortcut(shortcut)
, m_source(source)
{
}
KeySource source() const { return m_source; }
const ui::Shortcut& shortcut() const { return *this; }
// bool operator==(const AppShortcut& other) const { return shortcut.operator==(other.shortcut); }
// bool operator!=(const AppShortcut& other) const { return shortcut.operator!=(other.shortcut); }
// Returns true if this AppShortcut is better for the current
// context, compared to another shortcut.
bool fitsBetterThan(KeyContext currentContext,
KeyContext thisShortcutContext,
KeyContext otherShortcutContext,
const AppShortcut& otherShortcut) const;
private:
KeySource m_source;
};
using AppShortcuts = ui::ShortcutsT<AppShortcut>;
class Key;
using KeyPtr = std::shared_ptr<Key>;
using Keys = std::vector<KeyPtr>;
using KeySourceShortcutList = std::vector<std::pair<KeySource, ui::Shortcut>>;
using DragVector = base::Vector2d<double>;
// A set of key shortcuts (AppShortcuts) associated to one command,
// tool, or specific action.
class Key {
public:
Key(const Key& key);
@ -119,13 +149,15 @@ public:
static KeyPtr MakeDragAction(WheelAction dragAction);
KeyType type() const { return m_type; }
const ui::Shortcuts& shortcuts() const;
const KeySourceShortcutList& addsKeys() const { return m_adds; }
const KeySourceShortcutList& delsKeys() const { return m_dels; }
const AppShortcuts& shortcuts() const;
const AppShortcuts& addsKeys() const { return m_adds; }
const AppShortcuts& delsKeys() const { return m_dels; }
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 fitsContext(KeyContext keyContext) const;
const AppShortcut* isPressed(const ui::Message* msg, KeyContext keyContext) const;
const AppShortcut* isPressed(const ui::Message* msg) const;
bool isPressed() const;
bool isLooselyPressed() const;
bool isCommandListed() const;
@ -161,11 +193,11 @@ public:
private:
KeyType m_type;
KeySourceShortcutList m_adds;
KeySourceShortcutList m_dels;
AppShortcuts m_adds;
AppShortcuts m_dels;
// Final list of shortcuts after processing the
// addition/deletion of extension-defined & user-defined keys.
mutable std::unique_ptr<ui::Shortcuts> m_shortcuts;
mutable std::unique_ptr<AppShortcuts> m_shortcuts;
KeyContext m_keycontext;
// for KeyType::Command
@ -178,7 +210,13 @@ private:
DragVector m_dragVector; // for KeyType::DragAction
};
std::string convertKeyContextToUserFriendlyString(KeyContext keyContext);
// Clears collection with strings that depends on the current
// language, so they can be reconstructed when they are needed with a
// new selected language.
void reset_key_tables_that_depends_on_language();
std::string key_tooltip(const char* str, const Key* key);
std::string convert_keycontext_to_user_friendly_string(KeyContext keyctx);
} // namespace app
@ -194,6 +232,11 @@ app::WheelAction convert_to(const std::string& from);
template<>
std::string convert_to(const app::WheelAction& from);
template<>
app::KeyContext convert_to(const std::string& from);
template<>
std::string convert_to(const app::KeyContext& from);
} // namespace base
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2023 Igara Studio S.A.
// Copyright (C) 2023-2025 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -11,18 +11,59 @@
namespace app {
// Specifies a possible App state/context where the user can press a
// key and associate different actions/commands in that specific
// context. Useful to associate the same key to different actions
// depending on the current context.
//
// This list should be sorted in such a way that more specific
// contexts are below:
//
// Normal > SelectionTool > Transformation
//
// This means that keys defined in SelectionTool are still valid in
// Transformation context.
//
// Other key context are just temporary in the Editor, like a mouse
// drag operation when we are transforming the
// selection. E.g. TranslatingSelection, ScalingSelection, etc.
enum class KeyContext {
// Special context to define keys. This is not used as a "current
// key context", but just to define keys for the whole range of key
// contexts.
Any,
// Regular context in a standby user state, the user is not dragging
// or clicking the mouse.
Normal,
// The user has a selection on the canvas and a selection-like tool
// (selection ink) selected.
SelectionTool,
// Special context to modify a specific action when the mouse is
// above the handles to translate/scale/rotate or is already
// translating/scaling/rotating the selection.
TranslatingSelection,
ScalingSelection,
RotatingSelection,
// Special context when the user has a specific tool selected to
// modify the tool behavior with a key:
MoveTool,
FreehandTool,
ShapeTool,
// When the mouse wheel is used above the editor.
MouseWheel,
// The user has a range of frames/cels selected in the timeline.
FramesSelection,
// The user has moved the selection and has yet to drop the pixels
// (the user can drop or undo the transformation).
Transformation,
};
} // namespace app

View File

@ -22,6 +22,7 @@
#include "app/tools/ink.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
#include "app/ui/editor/editor.h"
#include "app/ui/key.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui_context.h"
@ -39,171 +40,10 @@
#define XML_KEYBOARD_FILE_VERSION "1"
#define I18N_KEY(a) app::Strings::keyboard_shortcuts_##a()
using namespace tinyxml2;
namespace {
struct KeyShortcutAction {
const char* name;
std::string userfriendly;
app::KeyAction action;
app::KeyContext context;
};
static std::vector<KeyShortcutAction> g_actions;
static const std::vector<KeyShortcutAction>& actions()
{
if (g_actions.empty()) {
g_actions = std::vector<KeyShortcutAction>{
{ "CopySelection",
I18N_KEY(copy_selection),
app::KeyAction::CopySelection,
app::KeyContext::TranslatingSelection },
{ "SnapToGrid",
I18N_KEY(snap_to_grid),
app::KeyAction::SnapToGrid,
app::KeyContext::TranslatingSelection },
{ "LockAxis",
I18N_KEY(lock_axis),
app::KeyAction::LockAxis,
app::KeyContext::TranslatingSelection },
{ "FineControl",
I18N_KEY(fine_translating),
app::KeyAction::FineControl,
app::KeyContext::TranslatingSelection },
{ "MaintainAspectRatio",
I18N_KEY(maintain_aspect_ratio),
app::KeyAction::MaintainAspectRatio,
app::KeyContext::ScalingSelection },
{ "ScaleFromCenter",
I18N_KEY(scale_from_center),
app::KeyAction::ScaleFromCenter,
app::KeyContext::ScalingSelection },
{ "FineControl",
I18N_KEY(fine_scaling),
app::KeyAction::FineControl,
app::KeyContext::ScalingSelection },
{ "AngleSnap",
I18N_KEY(angle_snap),
app::KeyAction::AngleSnap,
app::KeyContext::RotatingSelection },
{ "AddSelection",
I18N_KEY(add_selection),
app::KeyAction::AddSelection,
app::KeyContext::SelectionTool },
{ "SubtractSelection",
I18N_KEY(subtract_selection),
app::KeyAction::SubtractSelection,
app::KeyContext::SelectionTool },
{ "IntersectSelection",
I18N_KEY(intersect_selection),
app::KeyAction::IntersectSelection,
app::KeyContext::SelectionTool },
{ "AutoSelectLayer",
I18N_KEY(auto_select_layer),
app::KeyAction::AutoSelectLayer,
app::KeyContext::MoveTool },
{ "StraightLineFromLastPoint",
I18N_KEY(line_from_last_point),
app::KeyAction::StraightLineFromLastPoint,
app::KeyContext::FreehandTool },
{ "AngleSnapFromLastPoint",
I18N_KEY(angle_from_last_point),
app::KeyAction::AngleSnapFromLastPoint,
app::KeyContext::FreehandTool },
{ "MoveOrigin",
I18N_KEY(move_origin),
app::KeyAction::MoveOrigin,
app::KeyContext::ShapeTool },
{ "SquareAspect",
I18N_KEY(square_aspect),
app::KeyAction::SquareAspect,
app::KeyContext::ShapeTool },
{ "DrawFromCenter",
I18N_KEY(draw_from_center),
app::KeyAction::DrawFromCenter,
app::KeyContext::ShapeTool },
{ "RotateShape",
I18N_KEY(rotate_shape),
app::KeyAction::RotateShape,
app::KeyContext::ShapeTool },
{ "LeftMouseButton",
I18N_KEY(trigger_left_mouse_button),
app::KeyAction::LeftMouseButton,
app::KeyContext::Any },
{ "RightMouseButton",
I18N_KEY(trigger_right_mouse_button),
app::KeyAction::RightMouseButton,
app::KeyContext::Any }
};
}
return g_actions;
}
static struct {
const char* name;
app::KeyContext context;
} g_contexts[] = {
{ "", app::KeyContext::Any },
{ "Normal", app::KeyContext::Normal },
{ "Selection", app::KeyContext::SelectionTool },
{ "TranslatingSelection", app::KeyContext::TranslatingSelection },
{ "ScalingSelection", app::KeyContext::ScalingSelection },
{ "RotatingSelection", app::KeyContext::RotatingSelection },
{ "MoveTool", app::KeyContext::MoveTool },
{ "FreehandTool", app::KeyContext::FreehandTool },
{ "ShapeTool", app::KeyContext::ShapeTool },
{ "FramesSelection", app::KeyContext::FramesSelection },
{ NULL, app::KeyContext::Any }
};
using Vec = app::DragVector;
struct KeyShortcutWheelAction {
const char* name;
const std::string userfriendly;
Vec vector;
};
static std::vector<KeyShortcutWheelAction> g_wheel_actions;
static const std::vector<KeyShortcutWheelAction>& wheel_actions()
{
if (g_wheel_actions.empty()) {
g_wheel_actions = std::vector<KeyShortcutWheelAction>{
{ "", "", Vec(0.0, 0.0) },
{ "Zoom", I18N_KEY(zoom), Vec(8.0, 0.0) },
{ "VScroll", I18N_KEY(scroll_vertically), Vec(4.0, 0.0) },
{ "HScroll", I18N_KEY(scroll_horizontally), Vec(4.0, 0.0) },
{ "FgColor", I18N_KEY(fg_color), Vec(8.0, 0.0) },
{ "BgColor", I18N_KEY(bg_color), Vec(8.0, 0.0) },
{ "FgTile", I18N_KEY(fg_tile), Vec(8.0, 0.0) },
{ "BgTile", I18N_KEY(bg_tile), Vec(8.0, 0.0) },
{ "Frame", I18N_KEY(change_frame), Vec(16.0, 0.0) },
{ "BrushSize", I18N_KEY(change_brush_size), Vec(4.0, 0.0) },
{ "BrushAngle", I18N_KEY(change_brush_angle), Vec(-4.0, 0.0) },
{ "ToolSameGroup", I18N_KEY(change_tool_same_group), Vec(8.0, 0.0) },
{ "ToolOtherGroup", I18N_KEY(change_tool), Vec(0.0, -8.0) },
{ "Layer", I18N_KEY(change_layer), Vec(0.0, 8.0) },
{ "InkType", I18N_KEY(change_ink_type), Vec(0.0, -16.0) },
{ "InkOpacity", I18N_KEY(change_ink_opacity), Vec(0.0, 1.0) },
{ "LayerOpacity", I18N_KEY(change_layer_opacity), Vec(0.0, 1.0) },
{ "CelOpacity", I18N_KEY(change_cel_opacity), Vec(0.0, 1.0) },
{ "Alpha", I18N_KEY(color_alpha), Vec(4.0, 0.0) },
{ "HslHue", I18N_KEY(color_hsl_hue), Vec(1.0, 0.0) },
{ "HslSaturation", I18N_KEY(color_hsl_saturation), Vec(4.0, 0.0) },
{ "HslLightness", I18N_KEY(color_hsl_lightness), Vec(0.0, 4.0) },
{ "HsvHue", I18N_KEY(color_hsv_hue), Vec(1.0, 0.0) },
{ "HsvSaturation", I18N_KEY(color_hsv_saturation), Vec(4.0, 0.0) },
{ "HsvValue", I18N_KEY(color_hsv_value), Vec(0.0, 4.0) }
};
}
return g_wheel_actions;
}
const char* get_shortcut(XMLElement* elem)
{
const char* shortcut = NULL;
@ -225,384 +65,12 @@ const char* get_shortcut(XMLElement* elem)
return shortcut;
}
std::string get_user_friendly_string_for_keyaction(app::KeyAction action, app::KeyContext context)
{
for (const auto& a : actions()) {
if (action == a.action && context == a.context)
return a.userfriendly;
}
return std::string();
}
std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelAction)
{
int c = int(wheelAction);
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last)) {
return wheel_actions()[c].userfriendly;
}
else
return std::string();
}
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 == shortcut) {
it = kvs.erase(it);
}
else
++it;
}
}
void erase_shortcuts(app::KeySourceShortcutList& kvs, const app::KeySource source)
{
for (auto it = kvs.begin(); it != kvs.end();) {
auto& kv = *it;
if (kv.first == source) {
it = kvs.erase(it);
}
else
++it;
}
}
} // anonymous namespace
namespace base {
template<>
app::KeyAction convert_to(const std::string& from)
{
for (const auto& a : actions()) {
if (from == a.name)
return a.action;
}
return app::KeyAction::None;
}
template<>
std::string convert_to(const app::KeyAction& from)
{
for (const auto& a : actions()) {
if (from == a.action)
return a.name;
}
return std::string();
}
template<>
app::WheelAction convert_to(const std::string& from)
{
for (int c = int(app::WheelAction::First); c <= int(app::WheelAction::Last); ++c) {
if (from == wheel_actions()[c].name)
return (app::WheelAction)c;
}
return app::WheelAction::None;
}
template<>
std::string convert_to(const app::WheelAction& from)
{
int c = int(from);
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last)) {
return wheel_actions()[c].name;
}
else
return std::string();
}
template<>
app::KeyContext convert_to(const std::string& from)
{
for (int c = 0; g_contexts[c].name; ++c) {
if (from == g_contexts[c].name)
return g_contexts[c].context;
}
return app::KeyContext::Any;
}
template<>
std::string convert_to(const app::KeyContext& from)
{
for (int c = 0; g_contexts[c].name; ++c) {
if (from == g_contexts[c].context)
return g_contexts[c].name;
}
return std::string();
}
} // namespace base
} // namespace
namespace app {
using namespace ui;
//////////////////////////////////////////////////////////////////////
// Key
Key::Key(const Key& k)
: m_type(k.m_type)
, m_adds(k.m_adds)
, m_dels(k.m_dels)
, m_keycontext(k.m_keycontext)
{
switch (m_type) {
case KeyType::Command:
m_command = k.m_command;
m_params = k.m_params;
break;
case KeyType::Tool:
case KeyType::Quicktool: m_tool = k.m_tool; break;
case KeyType::Action: m_action = k.m_action; break;
case KeyType::WheelAction:
m_action = k.m_action;
m_wheelAction = k.m_wheelAction;
break;
case KeyType::DragAction:
m_action = k.m_action;
m_wheelAction = k.m_wheelAction;
m_dragVector = k.m_dragVector;
break;
}
}
Key::Key(Command* command, const Params& params, const KeyContext keyContext)
: m_type(KeyType::Command)
, m_keycontext(keyContext)
, m_command(command)
, m_params(params)
{
}
Key::Key(const KeyType type, tools::Tool* tool)
: m_type(type)
, m_keycontext(KeyContext::Any)
, m_tool(tool)
{
}
Key::Key(const KeyAction action, const KeyContext keyContext)
: m_type(KeyType::Action)
, m_keycontext(keyContext)
, m_action(action)
{
if (m_keycontext != KeyContext::Any)
return;
// Automatic key context
switch (action) {
case KeyAction::None: m_keycontext = KeyContext::Any; break;
case KeyAction::CopySelection:
case KeyAction::SnapToGrid:
case KeyAction::LockAxis:
case KeyAction::FineControl: m_keycontext = KeyContext::TranslatingSelection; break;
case KeyAction::AngleSnap: m_keycontext = KeyContext::RotatingSelection; break;
case KeyAction::MaintainAspectRatio:
case KeyAction::ScaleFromCenter: m_keycontext = KeyContext::ScalingSelection; break;
case KeyAction::AddSelection:
case KeyAction::SubtractSelection:
case KeyAction::IntersectSelection: m_keycontext = KeyContext::SelectionTool; break;
case KeyAction::AutoSelectLayer: m_keycontext = KeyContext::MoveTool; break;
case KeyAction::StraightLineFromLastPoint:
case KeyAction::AngleSnapFromLastPoint: m_keycontext = KeyContext::FreehandTool; break;
case KeyAction::MoveOrigin:
case KeyAction::SquareAspect:
case KeyAction::DrawFromCenter:
case KeyAction::RotateShape: m_keycontext = KeyContext::ShapeTool; break;
case KeyAction::LeftMouseButton: m_keycontext = KeyContext::Any; break;
case KeyAction::RightMouseButton: m_keycontext = KeyContext::Any; break;
}
}
Key::Key(const WheelAction wheelAction)
: m_type(KeyType::WheelAction)
, m_keycontext(KeyContext::MouseWheel)
, m_action(KeyAction::None)
, m_wheelAction(wheelAction)
{
}
// static
KeyPtr Key::MakeDragAction(WheelAction dragAction)
{
KeyPtr k(new Key(dragAction));
k->m_type = KeyType::DragAction;
k->m_keycontext = KeyContext::Any;
k->m_dragVector = wheel_actions()[(int)dragAction].vector;
return k;
}
const ui::Shortcuts& Key::shortcuts() const
{
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_shortcuts->add(kv.second);
}
// Delete/add extension-defined keys
for (const auto& kv : m_dels) {
if (kv.first == KeySource::ExtensionDefined)
m_shortcuts->remove(kv.second);
else {
ASSERT(kv.first != KeySource::Original);
}
}
for (const auto& kv : m_adds) {
if (kv.first == KeySource::ExtensionDefined)
m_shortcuts->add(kv.second);
}
// Delete/add user-defined keys
for (const auto& kv : m_dels) {
if (kv.first == KeySource::UserDefined)
m_shortcuts->remove(kv.second);
}
for (const auto& kv : m_adds) {
if (kv.first == KeySource::UserDefined)
m_shortcuts->add(kv.second);
}
}
return *m_shortcuts;
}
void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShortcuts& globalKeys)
{
m_adds.emplace_back(source, shortcut);
m_shortcuts.reset();
// Remove the shortcut from other commands
if (source == KeySource::ExtensionDefined || source == KeySource::UserDefined) {
erase_shortcut(m_dels, source, shortcut);
globalKeys.disableShortcut(shortcut, source, m_keycontext, this);
}
}
const ui::Shortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
{
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 &shortcut;
}
}
}
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 &shortcut;
}
}
}
return nullptr;
}
const ui::Shortcut* Key::isPressed(const Message* msg) const
{
return isPressed(msg, KeyboardShortcuts::getCurrentKeyContext());
}
bool Key::isPressed() const
{
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
return shortcut.isPressed();
});
}
bool Key::isLooselyPressed() const
{
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
return shortcut.isLooselyPressed();
});
}
bool Key::isCommandListed() const
{
return type() == KeyType::Command && command()->isListed(params());
}
bool Key::hasShortcut(const ui::Shortcut& shortcut) const
{
return shortcuts().has(shortcut);
}
bool Key::hasUserDefinedShortcuts() const
{
return std::any_of(m_adds.begin(), m_adds.end(), [](const auto& kv) {
return (kv.first == KeySource::UserDefined);
});
}
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 shortcut.
ASSERT(source != KeySource::Original);
erase_shortcut(m_adds, source, shortcut);
erase_shortcut(m_dels, source, shortcut);
m_dels.emplace_back(source, shortcut);
m_shortcuts.reset();
}
void Key::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_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_shortcuts.reset();
}
std::string Key::triggerString() const
{
switch (m_type) {
case KeyType::Command: m_command->loadParams(m_params); return m_command->friendlyName();
case KeyType::Tool:
case KeyType::Quicktool: {
std::string text = m_tool->getText();
if (m_type == KeyType::Quicktool)
text += " (quick)";
return text;
}
case KeyType::Action: return get_user_friendly_string_for_keyaction(m_action, m_keycontext);
case KeyType::WheelAction:
case KeyType::DragAction: return get_user_friendly_string_for_wheelaction(m_wheelAction);
}
return "Unknown";
}
//////////////////////////////////////////////////////////////////////
// KeyboardShortcuts
static std::unique_ptr<KeyboardShortcuts> g_singleton;
// static
@ -621,12 +89,10 @@ void KeyboardShortcuts::destroyInstance()
KeyboardShortcuts::KeyboardShortcuts()
{
ASSERT(Strings::instance());
Strings::instance()->LanguageChange.connect([] {
// Clear collections so they are re-constructed with the new language
g_actions.clear();
g_wheel_actions.clear();
});
// Strings instance can be nullptr in tests.
if (auto* strings = Strings::instance()) {
strings->LanguageChange.connect([] { reset_key_tables_that_depends_on_language(); });
}
}
KeyboardShortcuts::~KeyboardShortcuts()
@ -903,12 +369,12 @@ void KeyboardShortcuts::exportKeys(XMLElement* parent, KeyType type)
continue;
for (const auto& kv : key->delsKeys())
if (kv.first == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv.second, true);
if (kv.source() == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv, true);
for (const auto& kv : key->addsKeys())
if (kv.first == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv.second, false);
if (kv.source() == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv, false);
}
}
@ -1078,6 +544,12 @@ void KeyboardShortcuts::disableShortcut(const ui::Shortcut& shortcut,
// static
KeyContext KeyboardShortcuts::getCurrentKeyContext()
{
// For shortcuts to Apply/Cancel transformation/moving pixels state.
auto* editor = Editor::activeEditor();
if (editor && editor->isMovingPixels()) {
return KeyContext::Transformation;
}
auto* ctx = UIContext::instance();
Doc* doc = ctx->activeDocument();
if (doc && doc->isMaskVisible() &&
@ -1104,23 +576,47 @@ KeyContext KeyboardShortcuts::getCurrentKeyContext()
return KeyContext::Normal;
}
bool KeyboardShortcuts::getCommandFromKeyMessage(const Message* msg,
Command** command,
Params* params)
KeyPtr KeyboardShortcuts::findBestKeyFromMessage(const ui::Message* msg,
KeyContext currentKeyContext,
std::optional<KeyType> filterByType) const
{
const KeyContext contexts[] = { getCurrentKeyContext(), KeyContext::Normal };
const KeyContext contexts[] = { currentKeyContext, KeyContext::Normal };
int n = (contexts[0] != contexts[1] ? 2 : 1);
KeyPtr bestKey = nullptr;
const AppShortcut* bestShortcut = nullptr;
for (int i = 0; i < n; ++i) {
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Command && key->isPressed(msg, contexts[i])) {
if (command)
*command = key->command();
if (params)
*params = key->params();
return true;
for (const KeyPtr& key : m_keys) {
// Skip keys that are not for the specific KeyType (e.g. only for commands).
if (filterByType.has_value() && key->type() != *filterByType)
continue;
const AppShortcut* shortcut = key->isPressed(msg, contexts[i]);
if (shortcut && (!bestKey || shortcut->fitsBetterThan(currentKeyContext,
key->keycontext(),
bestKey->keycontext(),
*bestShortcut))) {
bestKey = key;
bestShortcut = shortcut;
}
}
}
return bestKey;
}
bool KeyboardShortcuts::getCommandFromKeyMessage(const ui::Message* msg,
Command** command,
Params* params,
KeyContext currentKeyContext)
{
KeyPtr key = findBestKeyFromMessage(msg, currentKeyContext, std::make_optional(KeyType::Command));
if (key) {
ASSERT(key->type() == KeyType::Command);
if (command)
*command = key->command();
if (params)
*params = key->params();
return true;
}
return false;
}
@ -1164,10 +660,10 @@ WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext c
const ui::Message* msg)
{
WheelAction wheelAction = WheelAction::None;
const ui::Shortcut* bestShortcut = nullptr;
const AppShortcut* bestShortcut = nullptr;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::WheelAction && key->keycontext() == context) {
const ui::Shortcut* shortcut = key->isPressed(msg);
const AppShortcut* shortcut = key->isPressed(msg);
if ((shortcut) && (!bestShortcut || bestShortcut->modifiers() < shortcut->modifiers())) {
bestShortcut = shortcut;
wheelAction = key->wheelAction();
@ -1183,7 +679,7 @@ Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const ui::Message* msg)
Keys keys;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::DragAction) {
const ui::Shortcut* shortcut = key->isPressed(msg);
const AppShortcut* shortcut = key->isPressed(msg);
if (shortcut) {
keys.push_back(key);
}
@ -1309,34 +805,4 @@ void KeyboardShortcuts::addMissingKeysForCommands()
}
}
std::string key_tooltip(const char* str, const app::Key* key)
{
std::string res;
if (str)
res += str;
if (key && !key->shortcuts().empty()) {
res += " (";
res += key->shortcuts().front().toString();
res += ")";
}
return res;
}
std::string convertKeyContextToUserFriendlyString(KeyContext keyContext)
{
switch (keyContext) {
case KeyContext::Any: return std::string();
case KeyContext::Normal: return I18N_KEY(key_context_normal);
case KeyContext::SelectionTool: return I18N_KEY(key_context_selection);
case KeyContext::TranslatingSelection: return I18N_KEY(key_context_translating_selection);
case KeyContext::ScalingSelection: return I18N_KEY(key_context_scaling_selection);
case KeyContext::RotatingSelection: return I18N_KEY(key_context_rotating_selection);
case KeyContext::MoveTool: return I18N_KEY(key_context_move_tool);
case KeyContext::FreehandTool: return I18N_KEY(key_context_freehand_tool);
case KeyContext::ShapeTool: return I18N_KEY(key_context_shape_tool);
case KeyContext::FramesSelection: return I18N_KEY(key_context_frames_selection);
}
return std::string();
}
} // namespace app

View File

@ -12,6 +12,8 @@
#include "app/ui/key.h"
#include "obs/signal.h"
#include <optional>
namespace tinyxml2 {
class XMLElement;
}
@ -59,7 +61,18 @@ public:
const Key* newKey);
static KeyContext getCurrentKeyContext();
bool getCommandFromKeyMessage(const ui::Message* msg, Command** command, Params* params);
KeyPtr findBestKeyFromMessage(
const ui::Message* msg,
KeyContext currentKeyContext = KeyboardShortcuts::getCurrentKeyContext(),
std::optional<KeyType> filterByType = std::nullopt) const;
bool getCommandFromKeyMessage(
const ui::Message* msg,
Command** command,
Params* params,
KeyContext currentKeyContext = KeyboardShortcuts::getCurrentKeyContext());
tools::Tool* getCurrentQuicktool(tools::Tool* currentTool);
KeyAction getCurrentActionModifiers(KeyContext context);
WheelAction getWheelActionFromMouseMessage(KeyContext context, const ui::Message* msg);
@ -85,8 +98,6 @@ private:
mutable Keys m_keys;
};
std::string key_tooltip(const char* str, const Key* key);
inline std::string key_tooltip(const char* str,
const char* commandName,
const Params& params = Params(),

View File

@ -0,0 +1,184 @@
// 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 <gtest/gtest.h>
#include "app/ui/keyboard_shortcuts.h"
#include "app/app.h"
#include "app/cli/app_options.h"
#include "app/commands/command.h"
#include "app/commands/command_ids.h"
#include "os/system.h"
#include "ui/app_state.h"
#include "ui/message.h"
using namespace app;
using namespace ui;
static KeyboardShortcuts* ks = nullptr;
#define DEFINE_KEY(commandId, scancode, keycontext) \
{ \
KeyPtr k = ks->command(CommandId::commandId(), {}, keycontext); \
k->add(ui::Shortcut(kKeyNoneModifier, scancode, 0), KeySource::Original, *ks); \
}
#define DEFINE_USER_KEY(commandId, scancode, keycontext) \
{ \
KeyPtr k = ks->command(CommandId::commandId(), {}, keycontext); \
k->add(ui::Shortcut(kKeyNoneModifier, scancode, 0), KeySource::UserDefined, *ks); \
}
#define EXPECT_COMMAND_FOR_KEY(commandId, scancode, keycontext) \
{ \
KeyMessage msg(kKeyDownMessage, scancode, kKeyNoneModifier, 0, 0); \
Command* cmd = nullptr; \
EXPECT_TRUE(ks->getCommandFromKeyMessage(&msg, &cmd, nullptr, keycontext)); \
ASSERT_TRUE(cmd != nullptr) << "command not found for key"; \
EXPECT_EQ(CommandId::commandId(), cmd->id()) << "other command found: " << cmd->id(); \
}
#define NO_COMMAND_FOR_KEY(scancode, keycontext) \
{ \
KeyMessage msg(kKeyDownMessage, scancode, kKeyNoneModifier, 0, 0); \
Command* cmd = nullptr; \
EXPECT_FALSE(ks->getCommandFromKeyMessage(&msg, &cmd, nullptr, keycontext)); \
ASSERT_TRUE(cmd == nullptr) << "command found for key: " << cmd->id(); \
}
TEST(KeyboardShortcuts, Basic)
{
ks->clear();
KeyPtr k = ks->command(CommandId::Undo(), {}, KeyContext::Any);
// Simulate a key press and check that the command is not yet
// associated to the 'Z' key.
{
KeyMessage msg(kKeyDownMessage, kKeyZ, kKeyNoneModifier, 0, 0);
Command* cmd = nullptr;
Params params;
EXPECT_FALSE(ks->getCommandFromKeyMessage(&msg, &cmd, &params));
}
// Associate the command to the 'Z' key and check.
k->add(ui::Shortcut(kKeyNoneModifier, kKeyZ, 0), KeySource::Original, *ks);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyZ, KeyContext::Normal);
}
TEST(KeyboardShortcuts, KeyContexts)
{
ks->clear();
DEFINE_KEY(Cancel, kKeyEsc, KeyContext::Any);
DEFINE_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
DEFINE_KEY(PlayAnimation, kKeyEnter, KeyContext::Normal);
DEFINE_KEY(Clear, kKeyBackspace, KeyContext::Normal);
DEFINE_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
DEFINE_KEY(Apply, kKeyEnter, KeyContext::Transformation);
DEFINE_KEY(Cut, kKeyX, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Cancel, kKeyEsc, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(PlayAnimation, kKeyEnter, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(PlayAnimation, kKeyEnter, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Apply, kKeyEnter, KeyContext::Transformation);
EXPECT_COMMAND_FOR_KEY(Clear, kKeyBackspace, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Clear, kKeyBackspace, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Clear, kKeyBackspace, KeyContext::Transformation);
NO_COMMAND_FOR_KEY(kKeyX, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Cut, kKeyX, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Cut, kKeyX, KeyContext::Transformation);
}
TEST(KeyboardShortcuts, UserDefinedPriority)
{
ks->clear();
DEFINE_KEY(Undo, kKeyZ, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyZ, KeyContext::Normal);
DEFINE_USER_KEY(Redo, kKeyZ, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Redo, kKeyZ, KeyContext::Normal);
DEFINE_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
DEFINE_USER_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
DEFINE_USER_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Any);
EXPECT_COMMAND_FOR_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::SelectionTool);
}
TEST(KeyboardShortcuts, SpecificContextHasMorePriorityButNotIfItsUserDefined)
{
ks->clear();
DEFINE_KEY(Cancel, kKeyEsc, KeyContext::Any);
DEFINE_KEY(Undo, kKeyEsc, KeyContext::Transformation);
// Pressing "Esc" in "Transformation" context should run "Undo",
// although "Cancel" is defined for "Any" context, a more specific
// context should have more priority.
EXPECT_COMMAND_FOR_KEY(Undo, kKeyEsc, KeyContext::Transformation);
// But an user-defined key, even for Any context, will overwrite the
// app-defined shortcut in all contexts.
DEFINE_USER_KEY(Zoom, kKeyEsc, KeyContext::Any);
EXPECT_COMMAND_FOR_KEY(Zoom, kKeyEsc, KeyContext::Transformation);
}
// Test that we can configure the Left key to always Undo when the
// default configuration says that the Left key does other actions in
// different contexts.
//
// Related issue: https://github.com/aseprite/aseprite/issues/5390
TEST(KeyboardShortcuts, UndoWithLeftAndRight)
{
ks->clear();
DEFINE_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
DEFINE_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
// "Transformation" is a sub-context of "Selection" context
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::Transformation);
// Now we try defining the "Left" key for "Any" context overwriting it in all contexts.
DEFINE_USER_KEY(Undo, kKeyLeft, KeyContext::Any);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyLeft, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyLeft, KeyContext::Transformation);
}
TEST(KeyboardShortcuts, FramesSelection)
{
ks->clear();
DEFINE_KEY(LayerProperties, kKeyF2, KeyContext::Normal);
DEFINE_KEY(SetLoopSection, kKeyF2, KeyContext::FramesSelection);
EXPECT_COMMAND_FOR_KEY(LayerProperties, kKeyF2, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(SetLoopSection, kKeyF2, KeyContext::FramesSelection);
}
int app_main(int argc, char* argv[])
{
os::SystemRef system = os::System::make();
const char* argv2[] = { argv[0] };
app::AppOptions options(sizeof(argv2) / sizeof(argv2[0]), argv2);
app::App app;
app.initialize(options);
app.run(false);
ks = KeyboardShortcuts::instance();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -13,6 +13,7 @@
#include "app/app.h"
#include "app/color.h"
#include "app/color_spaces.h"
#include "app/color_utils.h"
#include "app/commands/commands.h"
#include "app/modules/gfx.h"
@ -307,7 +308,8 @@ public:
if (tileImage) {
int w = tileImage->width();
int h = tileImage->height();
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(w, h);
os::SurfaceRef surface =
os::System::instance()->makeRgbaSurface(w, h, get_current_color_space(g->display()));
convert_image_to_surface(tileImage.get(),
get_current_palette(),
surface.get(),

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) 2017 David Capello
//
// This program is distributed under the terms of
@ -181,8 +181,7 @@ void SliceWindow::onPivotChange()
void SliceWindow::onToggleUserData()
{
m_userDataView.toggleVisibility();
remapWindow();
manager()->invalidate();
expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
void SliceWindow::onModifyField(ui::Entry* entry, const Mods mods)

View File

@ -11,6 +11,7 @@
#include "app/ui/tabs.h"
#include "app/color_spaces.h"
#include "app/color_utils.h"
#include "app/modules/gfx.h"
#include "app/modules/gui.h"
@ -969,7 +970,10 @@ void Tabs::createFloatingUILayer(Tab* tab)
ASSERT(!m_floatingUILayer);
ui::Display* display = this->display();
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(tab->width, m_tabsHeight);
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(
tab->width,
m_tabsHeight,
get_current_color_space(display));
// Fill the surface with pink color
{

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-2016 David Capello
//
// This program is distributed under the terms of
@ -132,8 +132,7 @@ void TagWindow::onRepeatChange()
void TagWindow::onToggleUserData()
{
m_userDataView.toggleVisibility();
remapWindow();
manager()->invalidate();
expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
} // namespace app

View File

@ -2526,7 +2526,7 @@ void Timeline::drawCel(ui::Graphics* g,
if (!thumb_bounds.isEmpty()) {
if (os::SurfaceRef surface =
thumb::get_cel_thumbnail(cel, m_scaleUpToFit, thumb_bounds.size())) {
thumb::get_cel_thumbnail(g->display(), cel, m_scaleUpToFit, thumb_bounds.size())) {
const int t = std::clamp(thumb_bounds.w / 8, 4, 16);
draw_checkered_grid(g, thumb_bounds, gfx::Size(t, t), docPref());
@ -2618,7 +2618,8 @@ void Timeline::drawCelOverlay(ui::Graphics* g)
return;
gfx::Rect rc = m_sprite->bounds().fitIn(gfx::Rect(m_thumbnailsOverlayBounds).shrink(1));
if (os::SurfaceRef surface = thumb::get_cel_thumbnail(cel, m_scaleUpToFit, rc.size())) {
if (os::SurfaceRef surface =
thumb::get_cel_thumbnail(g->display(), cel, m_scaleUpToFit, rc.size())) {
draw_checkered_grid(g, rc, gfx::Size(8, 8) * ui::guiscale(), docPref());
g->drawRgbaSurface(surface.get(),
@ -4321,6 +4322,12 @@ void Timeline::clearAndInvalidateRange()
}
}
void Timeline::refresh()
{
regenerateRows();
invalidate();
}
app::gen::GlobalPref::Timeline& Timeline::timelinePref() const
{
return Preferences::instance().timeline;
@ -4545,7 +4552,7 @@ void Timeline::onDrag(ui::DragEvent& e)
m_range.clearRange();
setHot(hitTest(nullptr, e.position()));
switch (m_hot.part) {
case PART_NOTHING: invalidate();
case PART_NOTHING: invalidate(); [[fallthrough]];
case PART_ROW:
case PART_ROW_EYE_ICON:
case PART_ROW_CONTINUOUS_ICON:
@ -4576,7 +4583,7 @@ void Timeline::onDrop(ui::DragEvent& e)
// Determine at which frame and layer the content was dropped on.
frame_t frame = m_frame;
layer_t layerIndex = getLayerIndex(m_layer);
layer_t layerIndex = m_sprite->root()->getLayerIndex(m_layer);
InsertionPoint insert = InsertionPoint::BeforeLayer;
DroppedOn droppedOn = DroppedOn::Unspecified;
TRACE("m_dropRange.type() %d\n", m_dropRange.type());
@ -4602,7 +4609,7 @@ void Timeline::onDrop(ui::DragEvent& e)
break;
case Range::kLayers:
droppedOn = DroppedOn::Layer;
if (m_dropTarget.vhit != DropTarget::VeryBottom) {
if (m_dropTarget.vhit != DropTarget::VeryBottom && !m_dropRange.selectedLayers().empty()) {
auto* selectedLayer = *m_dropRange.selectedLayers().begin();
layerIndex = getLayerIndex(selectedLayer);
}

View File

@ -155,6 +155,8 @@ public:
void clearAndInvalidateRange();
void refresh();
protected:
bool onProcessMessage(ui::Message* msg) override;
void onInitTheme(ui::InitThemeEvent& ev) override;

View File

@ -10,6 +10,7 @@
#endif
#include "app/app.h"
#include "app/cmd/add_slice.h"
#include "app/cmd/clear_mask.h"
#include "app/cmd/deselect_mask.h"
#include "app/cmd/set_mask.h"
@ -20,6 +21,7 @@
#include "app/doc_api.h"
#include "app/doc_range.h"
#include "app/doc_range_ops.h"
#include "app/i18n/strings.h"
#include "app/modules/gfx.h"
#include "app/modules/gui.h"
#include "app/pref/preferences.h"
@ -32,6 +34,7 @@
#include "app/util/cel_ops.h"
#include "app/util/clipboard.h"
#include "app/util/new_image_from_mask.h"
#include "app/util/slice_utils.h"
#include "clip/clip.h"
#include "doc/algorithm/shrink_bounds.h"
#include "doc/blend_image.h"
@ -114,6 +117,9 @@ struct Clipboard::Data {
// Selected set of layers/layers/cels
ClipboardRange range;
// Selected slices
std::vector<Slice> slices;
Data() { range.observeUIContext(); }
~Data()
@ -132,6 +138,7 @@ struct Clipboard::Data {
picks.clear();
mask.reset();
range.invalidate();
slices.clear();
}
ClipboardFormat format() const
@ -146,6 +153,8 @@ struct Clipboard::Data {
return ClipboardFormat::PaletteEntries;
else if (tileset && picks.picks())
return ClipboardFormat::Tileset;
else if (!slices.empty())
return ClipboardFormat::Slices;
else
return ClipboardFormat::None;
}
@ -212,6 +221,7 @@ void Clipboard::setData(Image* image,
Mask* mask,
Palette* palette,
Tileset* tileset,
const std::vector<Slice*>* slices,
bool set_native_clipboard,
bool image_source_is_transparent)
{
@ -226,6 +236,11 @@ void Clipboard::setData(Image* image,
else
m_data->image.reset(image);
if (slices) {
for (auto* slice : *slices)
m_data->slices.push_back(*slice);
}
if (set_native_clipboard && use_native_clipboard()) {
// Copy tilemap to the native clipboard
if (isTilemap) {
@ -262,6 +277,7 @@ bool Clipboard::copyFromDocument(const Site& site, bool merged)
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
Tileset::MakeCopyCopyingImages(ts),
nullptr,
true, // set native clipboard
site.layer() && !site.layer()->isBackground());
@ -277,6 +293,7 @@ bool Clipboard::copyFromDocument(const Site& site, bool merged)
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
nullptr,
nullptr,
true, // set native clipboard
site.layer() && !site.layer()->isBackground());
@ -401,6 +418,7 @@ void Clipboard::copyImage(const Image* image, const Mask* mask, const Palette* p
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
nullptr,
nullptr,
App::instance()->isGui(),
false);
}
@ -415,6 +433,7 @@ void Clipboard::copyTilemap(const Image* image,
(mask ? new Mask(*mask) : nullptr),
(pal ? new Palette(*pal) : nullptr),
Tileset::MakeCopyCopyingImages(tileset),
nullptr,
true,
false);
}
@ -428,6 +447,7 @@ void Clipboard::copyPalette(const Palette* palette, const PalettePicks& picks)
nullptr,
new Palette(*palette),
nullptr,
nullptr,
false, // Don't touch the native clipboard now
false);
@ -438,6 +458,20 @@ void Clipboard::copyPalette(const Palette* palette, const PalettePicks& picks)
m_data->picks = picks;
}
void Clipboard::copySlices(const std::vector<Slice*> slices)
{
if (slices.empty())
return;
setData(nullptr,
nullptr,
nullptr,
nullptr,
&slices,
false, // Don't touch the native clipboard now
false);
}
void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* position)
{
const Site site = ctx->activeSite();
@ -782,6 +816,26 @@ void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* po
}
break;
}
case ClipboardFormat::Slices: {
auto& slices = m_data->slices;
if (slices.empty())
return;
ContextWriter writer(ctx);
Tx tx(writer, "Paste Slices");
editor->clearSlicesSelection();
for (auto& s : slices) {
Slice* slice = new Slice(s);
slice->setName(Strings::general_copy_of(slice->name()));
tx(new cmd::AddSlice(dstSpr, slice));
editor->selectSlice(slice);
}
tx.commit();
updateDstDoc = true;
break;
}
}
// Update all editors/views showing this document
@ -799,7 +853,7 @@ ImageRef Clipboard::getImage(Palette* palette)
Tileset* native_tileset = nullptr;
getNativeBitmap(&native_image, &native_mask, &native_palette, &native_tileset);
if (native_image) {
setData(native_image, native_mask, native_palette, native_tileset, false, false);
setData(native_image, native_mask, native_palette, native_tileset, nullptr, false, false);
}
}
if (m_data->palette && palette)

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
@ -23,6 +23,7 @@ class Image;
class Mask;
class Palette;
class PalettePicks;
class Slice;
class Tileset;
} // namespace doc
@ -45,6 +46,7 @@ enum class ClipboardFormat {
PaletteEntries,
Tilemap,
Tileset,
Slices,
};
class Clipboard : public ui::ClipboardDelegate {
@ -74,6 +76,7 @@ public:
const doc::Palette* pal,
const doc::Tileset* tileset);
void copyPalette(const doc::Palette* palette, const doc::PalettePicks& picks);
void copySlices(const std::vector<doc::Slice*> slices);
void paste(Context* ctx, const bool interactive, const gfx::Point* position = nullptr);
doc::ImageRef getImage(doc::Palette* palette);
@ -106,6 +109,7 @@ private:
doc::Mask* mask,
doc::Palette* palette,
doc::Tileset* tileset,
const std::vector<doc::Slice*>* slices,
bool set_native_clipboard,
bool image_source_is_transparent);
bool copyFromDocument(const Site& site, bool merged = false);

View File

@ -0,0 +1,42 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#include "app/util/slice_utils.h"
#include "app/context_access.h"
#include "app/site.h"
#include "doc/slice.h"
#include "doc/sprite.h"
#include "fmt/format.h"
namespace app {
std::string get_unique_slice_name(const doc::Sprite* sprite, const std::string& namePrefix)
{
std::string prefix = namePrefix.empty() ? "Slice" : namePrefix;
int max = 0;
for (doc::Slice* slice : sprite->slices())
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
return fmt::format("{} {}", prefix, max + 1);
}
std::vector<doc::Slice*> get_selected_slices(const Site& site)
{
std::vector<Slice*> selectedSlices;
if (site.sprite() && !site.selectedSlices().empty()) {
for (auto* slice : site.sprite()->slices()) {
if (site.selectedSlices().contains(slice->id())) {
selectedSlices.push_back(slice);
}
}
}
return selectedSlices;
}
} // namespace app

View File

@ -0,0 +1,30 @@
// 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_SLICE_UTILS_H_INCLUDED
#define APP_SLICE_UTILS_H_INCLUDED
#pragma once
#include <string>
#include <vector>
namespace doc {
class Slice;
class Sprite;
} // namespace doc
namespace app {
class Site;
std::string get_unique_slice_name(const doc::Sprite* sprite,
const std::string& namePrefix = std::string());
std::vector<doc::Slice*> get_selected_slices(const Site& site);
} // namespace app
#endif

View File

@ -45,6 +45,7 @@ add_library(doc-lib
image.cpp
image_impl.cpp
image_io.cpp
image_iterators2.cpp
layer.cpp
layer_io.cpp
layer_list.cpp

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2022 Igara Studio S.A.
// Copyright (c) 2019-2025 Igara Studio S.A.
// Copyright (c) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -397,73 +397,19 @@ static void set_lum(double& r, double& g, double& b, double l)
clip_color(r, g, b);
}
static inline uint8_t get_imin_channel(const double r, const double g, const double b)
{
// We use '<=' to get min so as to catch two channels being equal
if (r <= g && r <= b)
return 0b001;
if (g <= r && g <= b)
return 0b010;
return 0b100;
}
static inline uint8_t get_imax_channel(const double r, const double g, const double b)
{
if (r > g && r > b)
return 0b001;
if (g > r && g > b)
return 0b010;
return 0b100;
}
static inline uint8_t get_imid_channel(uint8_t imin, uint8_t imax)
{
// Getting the remaining channel through exclusion guarantees that it is neither min nor max
return (~(imax | imin)) & 0b111;
}
static inline double& index_to_ref(double& r, double& g, double& b, uint8_t i)
{
if (i == 0b001)
return r;
if (i == 0b010)
return g;
return b;
}
// TODO replace this with a better impl (and test this, not sure if it's correct)
static void set_sat(double& r, double& g, double& b, double s)
{
#undef MIN
#undef MAX
#undef MID
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
#define MID(x, y, z) \
((x) > (y) ? ((y) > (z) ? (y) : ((x) > (z) ? (z) : (x))) : \
((y) > (z) ? ((z) > (x) ? (z) : (x)) : (y)))
const double minv = std::min(std::min(r, g), b);
const double maxv = std::max(std::max(r, g), b);
const double range = maxv - minv;
// Fetch channel indices
const uint8_t imin = get_imin_channel(r, g, b);
const uint8_t imax = get_imax_channel(r, g, b);
const uint8_t imid = get_imid_channel(imin, imax);
// Map the indices for each channel to references
double& min = index_to_ref(r, g, b, imin);
double& max = index_to_ref(r, g, b, imax);
double& mid = index_to_ref(r, g, b, imid);
if (max > min) {
mid = ((mid - min) * s) / (max - min);
max = s;
if (range > 0.0) {
r = ((r - minv) * s) / range;
g = ((g - minv) * s) / range;
b = ((b - minv) * s) / range;
}
else
mid = max = 0;
min = 0;
r = g = b = 0.0;
}
color_t rgba_blender_hsl_hue(color_t backdrop, color_t src, int opacity)
@ -552,7 +498,7 @@ color_t rgba_blender_subtract(color_t backdrop, color_t src, int opacity)
int r = rgba_getr(backdrop) - rgba_getr(src);
int g = rgba_getg(backdrop) - rgba_getg(src);
int b = rgba_getb(backdrop) - rgba_getb(src);
src = rgba(MAX(r, 0), MAX(g, 0), MAX(b, 0), 0) | (src & rgba_a_mask);
src = rgba(std::max(r, 0), std::max(g, 0), std::max(b, 0), 0) | (src & rgba_a_mask);
return rgba_blender_normal(backdrop, src, opacity);
}

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2018-2020 Igara Studio S.A.
// Copyright (c) 2018-2024 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2018-2023 Igara Studio S.A.
// Copyright (c) 2018-2024 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -12,6 +12,7 @@
#include "doc/color.h"
#include "doc/color_mode.h"
#include "doc/image_buffer.h"
#include "doc/image_iterators2.h"
#include "doc/image_spec.h"
#include "doc/object.h"
#include "doc/pixel_format.h"
@ -103,6 +104,21 @@ public:
virtual void fillRect(int x1, int y1, int x2, int y2, color_t color) = 0;
virtual void blendRect(int x1, int y1, int x2, int y2, color_t color, int opacity) = 0;
ReadIterator readArea() const { return ReadIterator(this, this->bounds()); }
WriteIterator writeArea() { return WriteIterator(this, this->bounds()); }
ReadIterator readArea(const gfx::Rect& bounds,
const IteratorStart start = IteratorStart::TopLeft) const
{
return ReadIterator(this, bounds, start);
}
WriteIterator writeArea(const gfx::Rect& bounds,
const IteratorStart start = IteratorStart::TopLeft)
{
return WriteIterator(this, bounds, start);
}
protected:
Image(const ImageSpec& spec);

170
src/doc/image_benchmark.cpp Normal file
View File

@ -0,0 +1,170 @@
// Aseprite Document Library
// Copyright (c) 2024 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/algorithm/random_image.h"
#include "doc/image.h"
#include "doc/image_ref.h"
#include <benchmark/benchmark.h>
using namespace doc;
void BM_ForXYGetPixel(benchmark::State& state)
{
const auto pf = (PixelFormat)state.range(0);
const int w = state.range(1);
const int h = state.range(2);
ImageRef img(Image::create(pf, w, h));
doc::algorithm::random_image(img.get());
for (auto _ : state) {
color_t c = 0;
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
c += img->getPixel(x, y);
}
}
benchmark::DoNotOptimize(c);
}
}
void BM_ForEachPixel(benchmark::State& state)
{
const auto pf = (PixelFormat)state.range(0);
const int w = state.range(1);
const int h = state.range(2);
ImageRef img(Image::create(pf, w, h));
doc::algorithm::random_image(img.get());
for (auto _ : state) {
color_t c = 0;
auto func = [&c](color_t u) { c += u; };
switch (pf) {
case IMAGE_RGB: for_each_pixel<RgbTraits>(img.get(), func); break;
case IMAGE_GRAYSCALE: for_each_pixel<GrayscaleTraits>(img.get(), func); break;
case IMAGE_INDEXED: for_each_pixel<IndexedTraits>(img.get(), func); break;
case IMAGE_BITMAP: for_each_pixel<BitmapTraits>(img.get(), func); break;
case IMAGE_TILEMAP: for_each_pixel<TilemapTraits>(img.get(), func); break;
}
benchmark::DoNotOptimize(c);
}
}
void BM_ForXYGetPixelFast(benchmark::State& state)
{
const auto pf = (PixelFormat)state.range(0);
const int w = state.range(1);
const int h = state.range(2);
ImageRef img(Image::create(pf, w, h));
doc::algorithm::random_image(img.get());
for (auto _ : state) {
color_t c = 0;
switch (pf) {
case IMAGE_RGB:
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
c += get_pixel_fast<RgbTraits>(img.get(), x, y);
}
}
break;
case IMAGE_GRAYSCALE:
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
c += get_pixel_fast<GrayscaleTraits>(img.get(), x, y);
}
}
break;
case IMAGE_INDEXED:
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
c += get_pixel_fast<IndexedTraits>(img.get(), x, y);
}
}
break;
case IMAGE_BITMAP:
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
c += get_pixel_fast<BitmapTraits>(img.get(), x, y);
}
}
break;
}
benchmark::DoNotOptimize(c);
}
}
template<typename Func>
void dispatch_by_pixel_format(Image* image, const ReadIterator& it, Func func)
{
switch (image->pixelFormat()) {
case IMAGE_RGB: func(it.addr32()); break;
case IMAGE_GRAYSCALE: func(it.addr16()); break;
case IMAGE_INDEXED: func(it.addr8()); break;
#if !DOC_USE_BITMAP_AS_1BPP
case IMAGE_BITMAP: func(it.addr8()); break;
#endif
case IMAGE_TILEMAP: func(it.addr32()); break;
}
}
void BM_ReadIterator(benchmark::State& state)
{
const auto pf = (PixelFormat)state.range(0);
const int w = state.range(1);
const int h = state.range(2);
ImageRef img(Image::create(pf, w, h));
doc::algorithm::random_image(img.get());
for (auto _ : state) {
auto it = img->readArea(img->bounds());
while (it.nextLine()) {
dispatch_by_pixel_format(img.get(), it, [w](auto addr) {
color_t c = 0;
for (int x = 0; x < w; ++x, ++addr) {
c += *addr;
}
benchmark::DoNotOptimize(c);
});
}
}
}
#define DEFARGS() \
->Args({ IMAGE_RGB, 1024, 1024 }) \
->Args({ IMAGE_GRAYSCALE, 1024, 1024 }) \
->Args({ IMAGE_INDEXED, 1024, 1024 }) \
->Args({ IMAGE_BITMAP, 1024, 1024 })
#if DOC_USE_BITMAP_AS_1BPP
// In this case we avoid IMAGE_BITMAP as its iterator goes through
// bits instead of bytes, i.e. a pixel cannot be addressed with a
// memory pointer.
#define DEFARGS_ADDRESSABLE_ONLY() \
->Args({ IMAGE_RGB, 1024, 1024 }) \
->Args({ IMAGE_GRAYSCALE, 1024, 1024 }) \
->Args({ IMAGE_INDEXED, 1024, 1024 })
#else
#define DEFARGS_ADDRESSABLE_ONLY DEFARGS
#endif
BENCHMARK(BM_ForXYGetPixel)
DEFARGS()->UseRealTime();
BENCHMARK(BM_ForEachPixel)
DEFARGS()->UseRealTime();
BENCHMARK(BM_ForXYGetPixelFast)
DEFARGS()->UseRealTime();
BENCHMARK(BM_ReadIterator)
DEFARGS_ADDRESSABLE_ONLY()->UseRealTime();
BENCHMARK_MAIN();

View File

@ -223,6 +223,26 @@ private:
//////////////////////////////////////////////////////////////////////
// Specializations
template<>
inline void ImageImpl<RgbTraits>::blendRect(int x1,
int y1,
int x2,
int y2,
color_t color,
int opacity)
{
address_t addr;
int x, y;
for (y = y1; y <= y2; ++y) {
addr = (address_t)getPixelAddress(x1, y);
for (x = x1; x <= x2; ++x) {
*addr = rgba_blender_normal(*addr, color, opacity);
++addr;
}
}
}
template<>
inline void ImageImpl<IndexedTraits>::clear(color_t color)
{
@ -237,6 +257,8 @@ inline void ImageImpl<BitmapTraits>::clear(color_t color)
std::fill(p, p + rowBytes() * height(), (color ? 0xff : 0x00));
}
#if DOC_USE_BITMAP_AS_1BPP
template<>
inline color_t ImageImpl<BitmapTraits>::getPixel(int x, int y) const
{
@ -267,26 +289,6 @@ inline void ImageImpl<BitmapTraits>::fillRect(int x1, int y1, int x2, int y2, co
ImageImpl<BitmapTraits>::drawHLine(x1, y, x2, color);
}
template<>
inline void ImageImpl<RgbTraits>::blendRect(int x1,
int y1,
int x2,
int y2,
color_t color,
int opacity)
{
address_t addr;
int x, y;
for (y = y1; y <= y2; ++y) {
addr = (address_t)getPixelAddress(x1, y);
for (x = x1; x <= x2; ++x) {
*addr = rgba_blender_normal(*addr, color, opacity);
++addr;
}
}
}
void copy_bitmaps(Image* dst, const Image* src, gfx::Clip area);
template<>
inline void ImageImpl<BitmapTraits>::copy(const Image* src, gfx::Clip area)
@ -294,6 +296,8 @@ inline void ImageImpl<BitmapTraits>::copy(const Image* src, gfx::Clip area)
copy_bitmaps(this, src, area);
}
#endif // DOC_USE_BITMAP_AS_1BPP
} // namespace doc
#endif

View File

@ -170,7 +170,9 @@ public:
};
//////////////////////////////////////////////////////////////////////
// Iterator for BitmapTraits
// Iterator for BitmapTraits when it's 1bpp
#if DOC_USE_BITMAP_AS_1BPP
class BitPixelAccess {
public:
@ -403,6 +405,8 @@ public:
}
};
#endif // DOC_USE_BITMAP_AS_1BPP
} // namespace doc
#endif

View File

@ -0,0 +1,39 @@
// Aseprite Document Library
// Copyright (c) 2024 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/image.h"
namespace doc {
ReadIterator::ReadIterator(const Image* image, const gfx::Rect& bounds, const IteratorStart start)
: m_rows(bounds.h)
{
switch (start) {
case IteratorStart::TopLeft:
m_addr = image->getPixelAddress(bounds.x, bounds.y);
m_nextRow = image->rowBytes();
break;
case IteratorStart::TopRight:
m_addr = image->getPixelAddress(bounds.x2() - 1, bounds.y);
m_nextRow = image->rowBytes();
break;
case IteratorStart::BottomLeft:
m_addr = image->getPixelAddress(bounds.x, bounds.y2() - 1);
m_nextRow = -image->rowBytes();
break;
case IteratorStart::BottomRight:
m_addr = image->getPixelAddress(bounds.x2() - 1, bounds.y2() - 1);
m_nextRow = -image->rowBytes();
break;
}
m_addr -= m_nextRow; // This is canceled in the first nextLine() call.
}
} // namespace doc

View File

@ -0,0 +1,74 @@
// Aseprite Document Library
// Copyright (c) 2024 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_IMAGE_ITERATOR2_H_INCLUDED
#define DOC_IMAGE_ITERATOR2_H_INCLUDED
#pragma once
#include "base/ints.h"
#include "gfx/fwd.h"
namespace doc {
class Image;
// New iterators classes.
enum class IteratorStart : uint8_t { TopLeft, TopRight, BottomLeft, BottomRight };
class ReadIterator {
public:
ReadIterator(const Image* image,
const gfx::Rect& bounds,
IteratorStart start = IteratorStart::TopLeft);
const uint8_t* addr8() const { return m_addr; }
const uint16_t* addr16() const { return (uint16_t*)m_addr; }
const uint32_t* addr32() const { return (uint32_t*)m_addr; }
template<typename ImageTraits>
typename ImageTraits::const_address_t addr() const
{
return (typename ImageTraits::const_address_t)m_addr;
}
bool nextLine()
{
m_addr += m_nextRow;
return (m_rows-- > 0);
}
protected:
uint8_t* m_addr = nullptr;
private:
int m_rows = 0;
int m_nextRow = 0;
};
class WriteIterator : public ReadIterator {
public:
WriteIterator(Image* image,
const gfx::Rect& bounds,
const IteratorStart start = IteratorStart::TopLeft)
: ReadIterator(image, bounds, start)
{
}
uint8_t* addr8() { return m_addr; }
uint16_t* addr16() { return (uint16_t*)m_addr; }
uint32_t* addr32() { return (uint32_t*)m_addr; }
template<typename ImageTraits>
uint32_t* addr()
{
return (typename ImageTraits::address_t)m_addr;
}
};
} // namespace doc
#endif

View File

@ -11,6 +11,7 @@
#include <gtest/gtest.h>
#include "doc/algorithm/random_image.h"
#include "doc/image.h"
#include "doc/primitives.h"
@ -25,9 +26,27 @@ protected:
ImageAllTypes() {}
};
typedef testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits, BitmapTraits> ImageAllTraits;
using ImageAllTraits = testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits, BitmapTraits>;
TYPED_TEST_SUITE(ImageAllTypes, ImageAllTraits);
#if DOC_USE_BITMAP_AS_1BPP
template<typename T>
class ImageAllTypesNoBitmap : public testing::Test {
protected:
ImageAllTypesNoBitmap() {}
};
using ImageAllTraitsNoBitmap = testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits>;
TYPED_TEST_SUITE(ImageAllTypesNoBitmap, ImageAllTraitsNoBitmap);
#else // !DOC_USE_BITMAP_AS_1BPP
#define ImageAllTypesNoBitmap ImageAllTypes
#endif // !DOC_USE_BITMAP_AS_1BPP
TYPED_TEST(ImageAllTypes, PutGetAndIterators)
{
using ImageTraits = TypeParam;
@ -195,6 +214,75 @@ TYPED_TEST(ImageAllTypes, FillRect)
}
}
TYPED_TEST(ImageAllTypesNoBitmap, NewIterators)
{
using ImageTraits = TypeParam;
for (int i = 0; i < 100; ++i) {
const int w = 1 + i;
const int h = 1 + i;
std::unique_ptr<Image> image(Image::create(ImageTraits::pixel_format, w, h));
doc::algorithm::random_image(image.get());
// TopLeft
{
int v = 0;
auto it = image->readArea(image->bounds(), IteratorStart::TopLeft);
while (it.nextLine()) {
auto* addr = (typename ImageTraits::address_t)it.addr8();
for (int u = 0; u < w; ++u, ++addr) {
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
ASSERT_EQ(expected, *addr);
}
++v;
}
}
// TopRight
{
int v = 0;
auto it = image->readArea(image->bounds(), IteratorStart::TopRight);
while (it.nextLine()) {
auto* addr = (typename ImageTraits::address_t)it.addr8();
for (int u = w - 1; u >= 0; --u, --addr) {
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
ASSERT_EQ(expected, *addr);
}
++v;
}
}
// BottomLeft
{
int v = h - 1;
auto it = image->readArea(image->bounds(), IteratorStart::BottomLeft);
while (it.nextLine()) {
auto* addr = (typename ImageTraits::address_t)it.addr8();
for (int u = 0; u < w; ++u, ++addr) {
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
ASSERT_EQ(expected, *addr);
}
--v;
}
}
// BottomRight
{
int v = h - 1;
auto it = image->readArea(image->bounds(), IteratorStart::BottomRight);
while (it.nextLine()) {
auto* addr = (typename ImageTraits::address_t)it.addr8();
for (int u = w - 1; u >= 0; --u, --addr) {
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
ASSERT_EQ(expected, *addr);
}
--v;
}
}
}
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);

View File

@ -140,6 +140,8 @@ struct IndexedTraits {
static inline bool same_color(const pixel_t a, const pixel_t b) { return a == b; }
};
#if DOC_USE_BITMAP_AS_1BPP
struct BitmapTraits {
static const ColorMode color_mode = ColorMode::BITMAP;
static const PixelFormat pixel_format = IMAGE_BITMAP;
@ -169,6 +171,47 @@ struct BitmapTraits {
static inline bool same_color(const pixel_t a, const pixel_t b) { return a == b; }
};
#else // !DOC_USE_BITMAP_AS_1BPP
struct BitmapTraits {
static const ColorMode color_mode = ColorMode::BITMAP;
static const PixelFormat pixel_format = IMAGE_BITMAP;
enum {
bits_per_pixel = 8,
bytes_per_pixel = 1,
pixels_per_byte = 1,
channels = 1,
has_alpha = false,
};
typedef uint8_t pixel_t;
typedef pixel_t* address_t;
typedef const pixel_t* const_address_t;
static const pixel_t min_value = 0x00;
static const pixel_t max_value = 0xff;
static inline int width_bytes(int pixels_per_row) { return bytes_per_pixel * pixels_per_row; }
static inline int rowstride_bytes(int pixels_per_row)
{
return doc_align_size(width_bytes(pixels_per_row));
}
static inline BlendFunc get_blender(BlendMode blend_mode, bool newBlend)
{
return get_indexed_blender(blend_mode, newBlend);
}
static inline bool same_color(const pixel_t a, const pixel_t b)
{
return (a == 0 && b == 0) || (a != 0 && b != 0);
}
};
#endif // !DOC_USE_BITMAP_AS_1BPP
struct TilemapTraits {
static const ColorMode color_mode = ColorMode::TILEMAP;
static const PixelFormat pixel_format = IMAGE_TILEMAP;

View File

@ -597,4 +597,22 @@ void LayerGroup::displaceFrames(frame_t fromThis, frame_t delta)
layer->displaceFrames(fromThis, delta);
}
layer_t LayerGroup::getLayerIndex(const Layer* layer, layer_t& index) const
{
for (Layer* child : this->layers()) {
if ((child->isGroup() && static_cast<LayerGroup*>(child)->getLayerIndex(layer, index) != -1) ||
(child == layer)) {
return index;
}
index++;
}
return -1;
}
layer_t LayerGroup::getLayerIndex(const Layer* layer) const
{
layer_t index = 0;
return this->getLayerIndex(layer, index);
}
} // namespace doc

View File

@ -236,9 +236,13 @@ public:
bool isBrowsable() const override { return isGroup() && isExpanded() && !m_layers.empty(); }
layer_t getLayerIndex(const Layer* layer) const;
private:
void destroyAllLayers();
layer_t getLayerIndex(const Layer* layer, layer_t& index) const;
LayerList m_layers;
};

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -46,24 +46,16 @@ void for_each_mask_pixel(Mask& a, const Mask& b, Func f)
Mask::Mask() : Object(ObjectType::Mask)
{
initialize();
}
Mask::Mask(const Mask& mask) : Object(mask)
{
initialize();
copyFrom(&mask);
}
Mask::~Mask()
{
ASSERT(m_freeze_count == 0);
}
void Mask::initialize()
{
m_freeze_count = 0;
m_bounds = gfx::Rect(0, 0, 0, 0);
ASSERT(m_freezes == 0);
}
int Mask::getMemSize() const
@ -78,17 +70,17 @@ void Mask::setName(const char* name)
void Mask::freeze()
{
ASSERT(m_freeze_count >= 0);
m_freeze_count++;
ASSERT(m_freezes >= 0);
m_freezes++;
}
void Mask::unfreeze()
{
ASSERT(m_freeze_count > 0);
m_freeze_count--;
ASSERT(m_freezes > 0);
m_freezes--;
// Shrink just in case
if (m_freeze_count == 0)
if (m_freezes == 0)
shrink();
}
@ -110,7 +102,7 @@ bool Mask::isRectangular() const
void Mask::copyFrom(const Mask* sourceMask)
{
ASSERT(m_freeze_count == 0);
ASSERT(m_freezes == 0);
clear();
setName(sourceMask->name().c_str());
@ -245,10 +237,10 @@ void Mask::intersect(const doc::Mask& mask)
void Mask::add(const gfx::Rect& bounds)
{
if (m_freeze_count == 0)
if (m_freezes == 0)
reserve(bounds);
// m_bitmap can be nullptr if we have m_freeze_count > 0
// m_bitmap can be nullptr if we have m_freezes > 0
if (!m_bitmap)
return;
@ -490,7 +482,7 @@ void Mask::reserve(const gfx::Rect& bounds)
void Mask::shrink()
{
// If the mask is frozen we avoid the shrinking
if (m_freeze_count > 0)
if (m_freezes > 0)
return;
#define SHRINK_SIDE(u_begin, u_op, u_final, u_add, v_begin, v_op, v_final, v_add, U, V, var) \

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