Compare commits

...

64 Commits

Author SHA1 Message Date
Gaspar Capello 86e920f6f5
Merge 3efd0014e8 into fae42dbe12 2025-09-12 11:54:59 -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
Christian Kaiser 4bb9239f50 [lua] Add `resizeable` property to Dialog constructor (fix #5177)
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-08-01 18:57:50 -03:00
David Capello cef92c1a38 Add .plist files for macOS
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
We don't have an Aseprite.app target in cmake files yet, but we might
add it in a near future.
2025-07-28 16:18:19 -03:00
Christian Kaiser 22e72ab5cb [win] Fix includeDesktopDir returning the default path
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
Uses SHGFP_TYPE_CURRENT which returns the Desktop that the user has configured instead of the default, fixes Windows 11's OneDrive Desktop folder.
2025-07-28 10:47:53 -03:00
Christian Kaiser 80fa065bd5 [lua] Add sprite.undoHistory
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-25 13:58:52 -03:00
David Capello de1ccb24dd [win] Don't drop text when IME dialog composition is accepted w/Enter
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
With #5230, now that we can show the IME dialog on Windows, when we
are selecting a specific word/composition in the IME dialog, if we
press Enter we'll receive that Enter onKeyUp(). It's better if we
process the Enter key onKeyDown() (as the IME enter key is not
received in that case).
2025-07-25 09:19:50 -03:00
David Capello 7d91c4b9d9 [win] Fix dead keys on Windows 2025-07-24 17:45:46 -03:00
Cerallin 6d89a6bc15 fix entry
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-24 12:50:32 -03:00
Cerallin d4e97b5a96 Add const method Entry::caretPosOnScreen()
This method is for Entry::setTextInput() and IME positioning.
2025-07-24 12:50:32 -03:00
Cerallin 205b18dc0f Make Entry::getCharBoxBounds() a const method 2025-07-24 12:50:32 -03:00
Gaspar Capello 3efd0014e8 Fix gif files to apply global palette in particular cases
This fix was prompted by Discord users who noticed that certain GIFs
saved with Aseprite and uploaded to Discord chats resulted in
animations whose colors changed unintentionally throughout
the animation. It was discovered that Discord does not correctly
process GIFs with disposal=DO_NOT_DISPOSE + global palette +
local palettes.

This fix essentially defines a global palette in all cases where
the animation (with Color Mode RGBA/GRAYSCALE) can be made with up to
256 global colors. This fixes simple animations from
a Discord perspective.

On the other hand, all other cases (color count > 256) continue to be
processed as before and may present issues in Discord.
2025-06-06 08:48:12 -03:00
107 changed files with 4424 additions and 2729 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

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

View File

@ -30,7 +30,7 @@
# HARFBUZZ_INCLUDE_DIRS - containg the HarfBuzz headers # HARFBUZZ_INCLUDE_DIRS - containg the HarfBuzz headers
# HARFBUZZ_LIBRARIES - containg the HarfBuzz library # HARFBUZZ_LIBRARIES - containg the HarfBuzz library
include(FindPkgConfig) find_package(PkgConfig QUIET)
pkg_check_modules(PC_HARFBUZZ harfbuzz>=0.9.7) 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"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite --> <!-- Aseprite -->
<!-- Copyright (C) 2018-2024 Igara Studio S.A. --> <!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
<!-- Copyright (C) 2001-2018 David Capello --> <!-- Copyright (C) 2001-2018 David Capello -->
<gui> <gui>
<!-- Keyboard shortcuts --> <!-- Keyboard shortcuts -->
@ -371,6 +371,12 @@
<param name="quantity" value="1" /> <param name="quantity" value="1" />
</key> </key>
<!-- Main selection actions (apply transformation / undo) -->
<key command="DeselectMask" shortcut="Esc" context="Transformation" />
<key command="Apply" shortcut="Enter" context="Transformation" />
<key command="Apply" shortcut="Enter Pad" context="Transformation" />
<key command="Undo" shortcut="Ctrl+Z" mac="Cmd+Z" context="Transformation" />
<!-- Move selection with arrows --> <!-- Move selection with arrows -->
<key command="MoveMask" shortcut="Left" context="Selection"> <key command="MoveMask" shortcut="Left" context="Selection">
<param name="target" value="content" /> <param name="target" value="content" />
@ -1203,6 +1209,7 @@
<menu id="slice_popup_menu"> <menu id="slice_popup_menu">
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" /> <item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
<item command="DuplicateSlice" text="@.duplicate" group="slice_popup_duplicate" />
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" /> <item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
</menu> </menu>

View File

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

View File

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

View File

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

2
laf

@ -1 +1 @@
Subproject commit a2bb9ec7fb98354279a2c49870a4a47a67a8e86e Subproject commit 5667a0334d0d04eb50aa935e7dafe1d4e0fa025b

View File

@ -180,8 +180,8 @@ if(ENABLE_ASEPRITE_EXE)
if(WIN32) if(WIN32)
set(main_resources set(main_resources
main/resources_win32.rc main/win/resources_win32.rc
main/settings.manifest) main/win/settings.manifest)
endif() endif()
add_executable(${main_target} add_executable(${main_target}

View File

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

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 // Aseprite
// Copyright (C) 2020-2023 Igara Studio S.A. // Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -101,12 +101,12 @@ public:
if (countCels() > 0) { if (countCels() > 0) {
m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()), m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()),
g_window->propertiesGrid()); propertiesGrid());
} }
else if (!m_cel) else if (!m_cel)
m_userDataView.setVisible(false, false); m_userDataView.setVisible(false, false);
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h)); expandWindow(gfx::Size(bounds().w, sizeHint().h));
updateFromCel(); updateFromCel();
} }
@ -281,7 +281,7 @@ private:
{ {
if (countCels() > 0) { if (countCels() > 0) {
m_userDataView.toggleVisibility(); m_userDataView.toggleVisibility();
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h)); expandWindow(gfx::Size(bounds().w, sizeHint().h));
} }
} }

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

@ -269,7 +269,7 @@ private:
if (m_key && m_key->keycontext() != KeyContext::Any) { if (m_key && m_key->keycontext() != KeyContext::Any) {
int w = m_headerItem->contextXPos() + 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); size.w = std::max(size.w, w);
} }
@ -317,7 +317,7 @@ private:
if (m_key && !m_key->shortcuts().empty()) { if (m_key && !m_key->shortcuts().empty()) {
if (m_key->keycontext() != KeyContext::Any) { if (m_key->keycontext() != KeyContext::Any) {
g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()), g->drawText(convert_keycontext_to_user_friendly_string(m_key->keycontext()),
fg, fg,
bg, bg,
gfx::Point(contextXPos, y)); gfx::Point(contextXPos, y));
@ -595,7 +595,7 @@ private:
case KeyContext::MoveTool: case KeyContext::MoveTool:
case KeyContext::FreehandTool: case KeyContext::FreehandTool:
case KeyContext::ShapeTool: case KeyContext::ShapeTool:
text = convertKeyContextToUserFriendlyString(key->keycontext()) + ": " + text; text = convert_keycontext_to_user_friendly_string(key->keycontext()) + ": " + text;
break; break;
} }
KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key, nullptr, 0, &m_headerItem); KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key, nullptr, 0, &m_headerItem);

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2020-2024 Igara Studio S.A. // Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -166,7 +166,7 @@ public:
m_document->add_observer(this); m_document->add_observer(this);
if (countLayers() > 0) { if (countLayers() > 0) {
m_userDataView.configureAndSet(m_layer->userData(), g_window->propertiesGrid()); m_userDataView.configureAndSet(m_layer->userData(), propertiesGrid());
if (m_remapAfterConfigure) { if (m_remapAfterConfigure) {
remapWindow(); remapWindow();
centerWindow(); centerWindow();
@ -368,8 +368,7 @@ private:
{ {
if (m_layer) { if (m_layer) {
m_userDataView.toggleVisibility(); m_userDataView.toggleVisibility();
g_window->remapWindow(); expandWindow(gfx::Size(bounds().w, sizeHint().h));
manager()->invalidate();
} }
} }

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -188,8 +188,7 @@ private:
void onToggleUserData() void onToggleUserData()
{ {
m_userDataView.toggleVisibility(); m_userDataView.toggleVisibility();
remapWindow(); expandWindow(gfx::Size(bounds().w, sizeHint().h));
manager()->invalidate();
} }
void onTilesedDuplicated(const Tileset* tilesetClone) void onTilesedDuplicated(const Tileset* tilesetClone)

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -8,6 +8,7 @@
FOR_EACH_COMMAND(About) FOR_EACH_COMMAND(About)
FOR_EACH_COMMAND(AddColor) FOR_EACH_COMMAND(AddColor)
FOR_EACH_COMMAND(AdvancedMode) FOR_EACH_COMMAND(AdvancedMode)
FOR_EACH_COMMAND(Apply)
FOR_EACH_COMMAND(AutocropSprite) FOR_EACH_COMMAND(AutocropSprite)
FOR_EACH_COMMAND(BackgroundFromLayer) FOR_EACH_COMMAND(BackgroundFromLayer)
FOR_EACH_COMMAND(BrightnessContrast) FOR_EACH_COMMAND(BrightnessContrast)
@ -40,6 +41,7 @@ FOR_EACH_COMMAND(DeselectMask)
FOR_EACH_COMMAND(Despeckle) FOR_EACH_COMMAND(Despeckle)
FOR_EACH_COMMAND(DiscardBrush) FOR_EACH_COMMAND(DiscardBrush)
FOR_EACH_COMMAND(DuplicateLayer) FOR_EACH_COMMAND(DuplicateLayer)
FOR_EACH_COMMAND(DuplicateSlice)
FOR_EACH_COMMAND(DuplicateSprite) FOR_EACH_COMMAND(DuplicateSprite)
FOR_EACH_COMMAND(DuplicateView) FOR_EACH_COMMAND(DuplicateView)
FOR_EACH_COMMAND(Exit) FOR_EACH_COMMAND(Exit)

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -338,6 +338,19 @@ void Doc::notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti)
notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev); notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev);
} }
void Doc::notifyBeforeSlicesDuplication()
{
DocEvent ev(this);
notify_observers<DocEvent&>(&DocObserver::onBeforeSlicesDuplication, ev);
}
void Doc::notifySliceDuplicated(Slice* slice)
{
DocEvent ev(this);
ev.slice(slice);
notify_observers<DocEvent&>(&DocObserver::onSliceDuplicated, ev);
}
bool Doc::isModified() const bool Doc::isModified() const
{ {
return !m_undo->isInSavedStateOrSimilar(); return !m_undo->isInSavedStateOrSimilar();

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -141,6 +141,8 @@ public:
void notifyTilesetChanged(Tileset* tileset); void notifyTilesetChanged(Tileset* tileset);
void notifyLayerGroupCollapseChange(Layer* layer); void notifyLayerGroupCollapseChange(Layer* layer);
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti); void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
void notifyBeforeSlicesDuplication();
void notifySliceDuplicated(Slice* slice);
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// File related properties // File related properties

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -90,6 +90,8 @@ public:
// Slices // Slices
virtual void onSliceNameChange(DocEvent& ev) {} virtual void onSliceNameChange(DocEvent& ev) {}
virtual void onBeforeSlicesDuplication(DocEvent& ev) {}
virtual void onSliceDuplicated(DocEvent& ev) {}
// The tileset has changed. // The tileset has changed.
virtual void onTilesetChanged(DocEvent& ev) {} virtual void onTilesetChanged(DocEvent& ev) {}

View File

@ -1050,7 +1050,47 @@ public:
m_fop->newBlend(), m_fop->newBlend(),
RgbMapAlgorithm::OCTREE, // TODO configurable? RgbMapAlgorithm::OCTREE, // TODO configurable?
false); // Do not add the transparent color yet false); // Do not add the transparent color yet
m_transparentIndex = 0;
m_globalColormapPalette = newPalette;
m_globalColormap = createColorMap(&m_globalColormapPalette);
}
}
// The following "if" block is intended to address cases where
// the Color Mode of the animation is RGB and can be represented by
// an absolute palette because it contains fewer than 256 colors
// throughout the entire animation. In this way the memory space
// used to generate the GIF is much more efficient, since local
// palettes don't need to be inserted in each frame. It also
// fixes a display issue with GIF files (generated by
// Aseprite 1.3.13) on Discord when the GIF parameters were:
// - disposal = DO_NOT_DISPOSE = 1
// - Local palettes exist.
if (m_spec.colorMode() == ColorMode::RGB || m_spec.colorMode() == ColorMode::GRAYSCALE) {
Palette newPalette(0, 512);
render::create_palette_from_sprite(m_sprite,
0,
totalFrames() - 1,
false,
&newPalette,
nullptr,
m_fop->newBlend(),
RgbMapAlgorithm::OCTREE,
false); // No effect on OctreeMap.
// Case: palette with (256 colors + mask color) == 257 but
// the mask color isn't used in the sprite.
if (newPalette.size() == 257 && !m_sprite->isColorUsed(0)) {
// Forcing GIF with background
m_transparentIndex = -1;
m_hasBackground = true;
// Discard the mask color (palette entry = 0)
for (int i = 0; i < 256; i++)
newPalette.setEntry(i, newPalette.getEntry(i + 1));
newPalette.resize(256);
m_globalColormapPalette = newPalette;
m_globalColormap = createColorMap(&m_globalColormapPalette);
}
else if (newPalette.size() <= 256) {
m_transparentIndex = 0; m_transparentIndex = 0;
m_globalColormapPalette = newPalette; m_globalColormapPalette = newPalette;
m_globalColormap = createColorMap(&m_globalColormapPalette); m_globalColormap = createColorMap(&m_globalColormapPalette);
@ -1095,6 +1135,9 @@ public:
gifframe_t nframes = totalFrames(); gifframe_t nframes = totalFrames();
for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) { for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) {
ASSERT(frame_it != frame_end); ASSERT(frame_it != frame_end);
if (m_fop->isStop())
break;
frame_t frame = *frame_it; frame_t frame = *frame_it;
++frame_it; ++frame_it;
@ -1388,15 +1431,18 @@ private:
if (!m_preservePaletteOrder) { if (!m_preservePaletteOrder) {
const LockImageBits<RgbTraits> srcBits(m_deltaImage.get()); const LockImageBits<RgbTraits> srcBits(m_deltaImage.get());
const LockImageBits<RgbTraits> preBits(m_previousImage, frameBounds);
LockImageBits<IndexedTraits> dstBits(frameImage.get()); LockImageBits<IndexedTraits> dstBits(frameImage.get());
auto srcIt = srcBits.begin(); auto srcIt = srcBits.begin();
auto dstIt = dstBits.begin(); auto dstIt = dstBits.begin();
auto preIt = preBits.begin();
for (int y = 0; y < frameBounds.h; ++y) { for (int y = 0; y < frameBounds.h; ++y) {
for (int x = 0; x < frameBounds.w; ++x, ++srcIt, ++dstIt) { for (int x = 0; x < frameBounds.w; ++x, ++srcIt, ++dstIt, ++preIt) {
ASSERT(srcIt != srcBits.end()); ASSERT(srcIt != srcBits.end());
ASSERT(dstIt != dstBits.end()); ASSERT(dstIt != dstBits.end());
ASSERT(preIt != preBits.end());
color_t color = *srcIt; color_t color = *srcIt;
int i; int i;
@ -1410,9 +1456,19 @@ private:
if (i < 0) if (i < 0)
i = octree.mapColor(color | rgba_a_mask); // alpha=255 i = octree.mapColor(color | rgba_a_mask); // alpha=255
} }
// If the alpha in a pixel from m_deltaImage is < 128, the
// pixel is assumed to be 0. Then it should draw the pixel
// according defined m_transparentIndex or disposal method
else { else {
if (m_transparentIndex >= 0) if (m_transparentIndex >= 0)
i = m_transparentIndex; i = m_transparentIndex;
else if (disposal == DisposalMethod::DO_NOT_DISPOSE) {
i = framePalette.findExactMatch(rgba_getr(*preIt),
rgba_getg(*preIt),
rgba_getb(*preIt),
255,
-1);
}
else else
i = m_bgIndex; i = m_bgIndex;
} }

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -219,7 +219,7 @@ void draw_tile(ui::Graphics* g, const Rect& rc, const Site& site, doc::tile_t ti
int w = tileImage->width(); int w = tileImage->width();
int h = tileImage->height(); 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());
convert_image_to_surface(tileImage.get(), get_current_palette(), surface.get(), 0, 0, 0, 0, w, h); convert_image_to_surface(tileImage.get(), get_current_palette(), surface.get(), 0, 0, 0, 0, w, h);
ui::Paint paint; ui::Paint paint;

View File

@ -681,12 +681,25 @@ void CustomizedGuiManager::onNewDisplayConfiguration(Display* display)
bool CustomizedGuiManager::processKey(Message* msg) bool CustomizedGuiManager::processKey(Message* msg)
{ {
App* app = App::instance(); App* app = App::instance();
const KeyContext currentCtx = KeyboardShortcuts::getCurrentKeyContext();
const KeyboardShortcuts* keys = KeyboardShortcuts::instance(); const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
const KeyContext contexts[] = { KeyboardShortcuts::getCurrentKeyContext(), KeyContext::Normal }; const KeyContext contexts[] = { currentCtx, KeyContext::Normal };
int n = (contexts[0] != contexts[1] ? 2 : 1); int n = (contexts[0] != contexts[1] ? 2 : 1);
// Find best match (prefer the shortcut that matches the context first)
KeyPtr key = nullptr;
for (int i = 0; i < n; ++i) { for (int i = 0; i < n; ++i) {
for (const KeyPtr& key : *keys) { for (const KeyPtr& k : *keys) {
if (key->isPressed(msg, contexts[i])) { if (k->isPressed(msg, contexts[i]) &&
(!key ||
(key->keycontext() != currentCtx && match_key_context(k->keycontext(), currentCtx)))) {
key = k;
}
}
}
if (!key)
return false;
// Cancel menu-bar loops (to close any popup menu) // Cancel menu-bar loops (to close any popup menu)
app->mainWindow()->getMenuBar()->cancelMenuLoop(); app->mainWindow()->getMenuBar()->cancelMenuLoop();
@ -708,8 +721,7 @@ bool CustomizedGuiManager::processKey(Message* msg)
bool done = false; bool done = false;
for (size_t i = 0; i < possibles.size(); ++i) { for (size_t i = 0; i < possibles.size(); ++i) {
if (possibles[i] != current_tool && if (possibles[i] != current_tool && ToolBar::instance()->isToolVisible(possibles[i])) {
ToolBar::instance()->isToolVisible(possibles[i])) {
select_this_tool = possibles[i]; select_this_tool = possibles[i];
done = true; done = true;
break; break;
@ -752,10 +764,7 @@ bool CustomizedGuiManager::processKey(Message* msg)
break; break;
} }
} }
break;
}
}
}
return false; return false;
} }

View File

@ -213,7 +213,7 @@ void ResourceFinder::includeDesktopDir(const char* filename)
#ifdef _WIN32 #ifdef _WIN32
std::vector<wchar_t> buf(MAX_PATH); std::vector<wchar_t> buf(MAX_PATH);
HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOPDIRECTORY, NULL, SHGFP_TYPE_DEFAULT, &buf[0]); HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOP, NULL, SHGFP_TYPE_CURRENT, &buf[0]);
if (hr == S_OK) { if (hr == S_OK) {
addPath(base::join_path(base::to_utf8(&buf[0]), filename)); addPath(base::join_path(base::to_utf8(&buf[0]), filename));
} }

View File

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

View File

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

View File

@ -1,11 +1,12 @@
// Aseprite // Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A. // Copyright (c) 2022-2025 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
#include "app/script/canvas_widget.h" #include "app/script/canvas_widget.h"
#include "app/color_spaces.h"
#include "app/script/graphics_context.h" #include "app/script/graphics_context.h"
#include "app/ui/skin/skin_theme.h" #include "app/ui/skin/skin_theme.h"
#include "os/system.h" #include "os/system.h"
@ -15,6 +16,10 @@
#include "ui/size_hint_event.h" #include "ui/size_hint_event.h"
#include "ui/system.h" #include "ui/system.h"
#ifdef LAF_SKIA
#include "os/skia/skia_color_space.h"
#endif
namespace app { namespace script { namespace app { namespace script {
// static // static
@ -45,7 +50,13 @@ void Canvas::callPaint()
return; return;
os::Paint p; os::Paint p;
#ifdef LAF_SKIA
if (m_surface && m_surface->colorSpace())
p.color(bgColor(), m_surface->colorSpace().get());
else
#else
p.color(bgColor()); p.color(bgColor());
#endif
m_surface->drawRect(m_surface->bounds(), p); m_surface->drawRect(m_surface->bounds(), p);
// Draw only on resize (onPaint we draw the cached m_surface) // Draw only on resize (onPaint we draw the cached m_surface)
@ -189,7 +200,7 @@ void Canvas::onResize(ui::ResizeEvent& ev)
} }
if (!m_surface || m_surface->width() != w || m_surface->height() != h) { if (!m_surface || m_surface->width() != w || m_surface->height() != h) {
m_surface = system->makeSurface(w, h); m_surface = system->makeSurface(w, h, get_current_color_space());
callPaint(); callPaint();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,8 @@
#include "app/console.h" #include "app/console.h"
#include "app/context.h" #include "app/context.h"
#include "app/context_observer.h" #include "app/context_observer.h"
#include "app/doc.h"
#include "app/doc_undo.h"
#include "app/script/docobj.h" #include "app/script/docobj.h"
#include "app/script/engine.h" #include "app/script/engine.h"
#include "app/script/luacpp.h" #include "app/script/luacpp.h"

View File

@ -64,6 +64,7 @@
#include "doc/tag.h" #include "doc/tag.h"
#include "doc/tileset.h" #include "doc/tileset.h"
#include "doc/tilesets.h" #include "doc/tilesets.h"
#include "undo/undo_state.h"
#include <algorithm> #include <algorithm>
@ -456,7 +457,7 @@ int Sprite_newCel(lua_State* L)
auto sprite = get_docobj<Sprite>(L, 1); auto sprite = get_docobj<Sprite>(L, 1);
auto layerBase = get_docobj<Layer>(L, 2); auto layerBase = get_docobj<Layer>(L, 2);
if (!layerBase->isImage()) if (!layerBase->isImage())
return luaL_error(L, "unexpected kinf of layer in Sprite:newCel()"); return luaL_error(L, "unexpected kind of layer in Sprite:newCel()");
frame_t frame = get_frame_number_from_arg(L, 3); frame_t frame = get_frame_number_from_arg(L, 3);
if (frame < 0 || frame > sprite->lastFrame()) if (frame < 0 || frame > sprite->lastFrame())
@ -1029,6 +1030,42 @@ int Sprite_set_useLayerUuids(lua_State* L)
return 0; return 0;
} }
int Sprite_get_undoHistory(lua_State* L)
{
const auto* sprite = get_docobj<Sprite>(L, 1);
const auto* doc = static_cast<Doc*>(sprite->document());
const auto* history = doc->undoHistory();
if (!history) {
lua_pushnil(L);
return 1;
}
const undo::UndoState* currentState = history->currentState();
const undo::UndoState* s = history->firstState();
const bool canRedo = history->canRedo();
bool pastCurrent = !currentState && canRedo;
int undoSteps = 0;
int redoSteps = 0;
while (s) {
if (pastCurrent && canRedo)
redoSteps++;
else if (currentState || !canRedo)
undoSteps++;
if (s == currentState || !currentState)
pastCurrent = true;
s = s->next();
}
lua_newtable(L);
setfield_integer(L, "undoSteps", undoSteps);
setfield_integer(L, "redoSteps", redoSteps);
return 1;
}
const luaL_Reg Sprite_methods[] = { const luaL_Reg Sprite_methods[] = {
{ "__eq", Sprite_eq }, { "__eq", Sprite_eq },
{ "resize", Sprite_resize }, { "resize", Sprite_resize },
@ -1094,6 +1131,7 @@ const Property Sprite_properties[] = {
{ "events", Sprite_get_events, nullptr }, { "events", Sprite_get_events, nullptr },
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin }, { "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids }, { "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
{ "undoHistory", Sprite_get_undoHistory, nullptr },
{ nullptr, nullptr, nullptr } { nullptr, nullptr, nullptr }
}; };

View File

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

View File

@ -10,6 +10,7 @@
#include "config.h" #include "config.h"
#endif #endif
#include "app/color_spaces.h"
#include "app/util/conversion_to_surface.h" #include "app/util/conversion_to_surface.h"
#include "doc/blend_mode.h" #include "doc/blend_mode.h"
#include "doc/cel.h" #include "doc/cel.h"
@ -55,7 +56,8 @@ os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
if (os::SurfaceRef thumbnail = os::System::instance()->makeRgbaSurface( if (os::SurfaceRef thumbnail = os::System::instance()->makeRgbaSurface(
thumbnailImage->width(), thumbnailImage->width(),
thumbnailImage->height())) { thumbnailImage->height(),
get_current_color_space())) {
convert_image_to_surface(thumbnailImage.get(), convert_image_to_surface(thumbnailImage.get(),
palette, palette,
thumbnail.get(), thumbnail.get(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello // Copyright (C) 2001-2017 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -94,7 +94,10 @@ private:
void onBeforeCommandExecution(CommandExecutionEvent& ev); void onBeforeCommandExecution(CommandExecutionEvent& ev);
void setTransparentColor(bool opaque, const app::Color& color); void setTransparentColor(bool opaque, const app::Color& color);
void deselect();
void dropPixels(); void dropPixels();
void cancelDrag();
bool isActiveDocument() const; bool isActiveDocument() const;
bool isActiveEditor() const; bool isActiveEditor() const;

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -45,6 +45,7 @@
#include "app/ui_context.h" #include "app/ui_context.h"
#include "app/util/expand_cel_canvas.h" #include "app/util/expand_cel_canvas.h"
#include "app/util/layer_utils.h" #include "app/util/layer_utils.h"
#include "app/util/slice_utils.h"
#include "doc/brush.h" #include "doc/brush.h"
#include "doc/cel.h" #include "doc/cel.h"
#include "doc/image.h" #include "doc/image.h"
@ -693,7 +694,7 @@ public:
// popup menu to create a new one. // popup menu to create a new one.
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) { if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
Slice* slice = new Slice; Slice* slice = new Slice;
slice->setName(getUniqueSliceName()); slice->setName(get_unique_slice_name(m_sprite));
SliceKey key(bounds); SliceKey key(bounds);
slice->insert(getFrame(), key); slice->insert(getFrame(), key);
@ -716,18 +717,6 @@ private:
// EditorObserver impl // EditorObserver impl
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); } void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); } void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
std::string getUniqueSliceName() const
{
std::string prefix = "Slice";
int max = 0;
for (Slice* slice : m_sprite->slices())
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
return fmt::format("{} {}", prefix, max + 1);
}
}; };
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2020-2022 Igara Studio S.A. // Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello // Copyright (C) 2001-2017 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -209,7 +209,8 @@ gfx::Rect TransformHandles::getPivotHandleBounds(Editor* editor,
{ {
auto theme = SkinTheme::get(editor); auto theme = SkinTheme::get(editor);
gfx::Size partSize = theme->parts.pivotHandle()->size(); gfx::Size partSize = theme->parts.pivotHandle()->size();
gfx::Point screenPivotPos = editor->editorToScreen(gfx::Point(transform.pivot())); gfx::Point pivotPos = gfx::Point(transform.pivot()) + editor->mainTilePosition();
gfx::Point screenPivotPos = editor->editorToScreen(pivotPos);
screenPivotPos.x += editor->projection().applyX(1) / 2; screenPivotPos.x += editor->projection().applyX(1) / 2;
screenPivotPos.y += editor->projection().applyY(1) / 2; screenPivotPos.y += editor->projection().applyY(1) / 2;

View File

@ -443,12 +443,7 @@ bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
return true; return true;
} }
bool WritingTextState::onKeyDown(Editor*, KeyMessage*) bool WritingTextState::onKeyDown(Editor*, KeyMessage* msg)
{
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
{ {
// Cancel loop pressing Esc key // Cancel loop pressing Esc key
if (msg->scancode() == ui::kKeyEsc) { if (msg->scancode() == ui::kKeyEsc) {
@ -457,7 +452,17 @@ bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
// Drop text pressing Enter key // Drop text pressing Enter key
else if (msg->scancode() == ui::kKeyEnter) { else if (msg->scancode() == ui::kKeyEnter) {
drop(); drop();
return true;
} }
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
{
// Note: We cannot process kKeyEnter key here to drop the text as it
// could be received after the Enter key is pressed in the IME
// dialog to accept the composition (not to accept the text). So we
// process kKeyEnter in onKeyDown().
return true; return true;
} }

View File

@ -20,9 +20,11 @@
#include "base/convert_to.h" #include "base/convert_to.h"
#include "base/scoped_value.h" #include "base/scoped_value.h"
#include "fmt/format.h" #include "fmt/format.h"
#include "text/font_style_set.h"
#include "ui/display.h" #include "ui/display.h"
#include "ui/fit_bounds.h" #include "ui/fit_bounds.h"
#include "ui/manager.h" #include "ui/manager.h"
#include "ui/menu.h"
#include "ui/message.h" #include "ui/message.h"
#include "ui/scale.h" #include "ui/scale.h"
@ -271,7 +273,7 @@ FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, tru
addItem("..."); addItem("...");
setMultiMode(MultiMode::Set); 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(1), Strings::text_tool_italic(), BOTTOM);
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), 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; m_info = info;
if (fromField != From::Face) auto family = theme()->fontMgr()->matchFamily(m_info.name());
m_face.setText(info.title()); 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) { if (fromField != From::Size) {
m_size.updateForFont(info); m_size.updateForFont(m_info);
m_size.setValue(fmt::format("{}", info.size())); 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) { if (fromField != From::Style) {
m_style.getItem(0)->setSelected(info.style().weight() >= text::FontStyle::Weight::SemiBold); m_style.getItem(1)->setSelected(m_info.style().slant() != text::FontStyle::Slant::Upright);
m_style.getItem(1)->setSelected(info.style().slant() != text::FontStyle::Slant::Upright);
} }
FontChange(m_info, fromField); FontChange(m_info, fromField);
@ -430,14 +487,48 @@ void FontEntry::onStyleItemClick(ButtonSet::Item* item)
switch (m_style.getItemIndex(item)) { switch (m_style.getItemIndex(item)) {
// Bold button changed // Bold button changed
case 0: { case 0: {
const bool bold = m_style.getItem(0)->isSelected(); if (m_availableWeights.size() > 2) {
// Ensure consistency, since the click also affects the "selected" highlighting.
item->setSelected(style.weight() != text::FontStyle::Weight::Normal);
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( style = text::FontStyle(
bold ? text::FontStyle::Weight::Bold : text::FontStyle::Weight::Normal, isBold ? text::FontStyle::Weight::Normal : text::FontStyle::Weight::Bold,
style.width(), style.width(),
style.slant()); style.slant());
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()), setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
From::Style); From::Style);
}
break; break;
} }
// Italic button changed // Italic button changed

View File

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

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

@ -0,0 +1,593 @@
// 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::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 {};
}
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;
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 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 || match_key_context(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";
}
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

@ -178,7 +178,13 @@ private:
DragVector m_dragVector; // for KeyType::DragAction 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 } // namespace app
@ -194,6 +200,11 @@ app::WheelAction convert_to(const std::string& from);
template<> template<>
std::string convert_to(const app::WheelAction& from); 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 } // namespace base
#endif #endif

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2023 Igara Studio S.A. // Copyright (C) 2023-2025 Igara Studio S.A.
// Copyright (C) 2017 David Capello // Copyright (C) 2017 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -23,8 +23,16 @@ enum class KeyContext {
ShapeTool, ShapeTool,
MouseWheel, MouseWheel,
FramesSelection, FramesSelection,
Transformation,
}; };
inline bool match_key_context(const KeyContext a, const KeyContext b)
{
return (a == b) || (a == KeyContext::Any || b == KeyContext::Any) ||
((a == KeyContext::SelectionTool && b == KeyContext::Transformation) ||
(a == KeyContext::Transformation && b == KeyContext::SelectionTool));
}
} // namespace app } // namespace app
#endif #endif

View File

@ -22,6 +22,7 @@
#include "app/tools/ink.h" #include "app/tools/ink.h"
#include "app/tools/tool.h" #include "app/tools/tool.h"
#include "app/tools/tool_box.h" #include "app/tools/tool_box.h"
#include "app/ui/editor/editor.h"
#include "app/ui/key.h" #include "app/ui/key.h"
#include "app/ui/timeline/timeline.h" #include "app/ui/timeline/timeline.h"
#include "app/ui_context.h" #include "app/ui_context.h"
@ -39,171 +40,10 @@
#define XML_KEYBOARD_FILE_VERSION "1" #define XML_KEYBOARD_FILE_VERSION "1"
#define I18N_KEY(a) app::Strings::keyboard_shortcuts_##a()
using namespace tinyxml2; using namespace tinyxml2;
namespace { 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* get_shortcut(XMLElement* elem)
{ {
const char* shortcut = NULL; const char* shortcut = NULL;
@ -225,384 +65,12 @@ const char* get_shortcut(XMLElement* elem)
return shortcut; return shortcut;
} }
std::string get_user_friendly_string_for_keyaction(app::KeyAction action, app::KeyContext context) } // namespace
{
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 app { namespace app {
using namespace ui; 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 std::unique_ptr<KeyboardShortcuts> g_singleton;
// static // static
@ -622,11 +90,7 @@ void KeyboardShortcuts::destroyInstance()
KeyboardShortcuts::KeyboardShortcuts() KeyboardShortcuts::KeyboardShortcuts()
{ {
ASSERT(Strings::instance()); ASSERT(Strings::instance());
Strings::instance()->LanguageChange.connect([] { Strings::instance()->LanguageChange.connect([] { reset_key_tables_that_depends_on_language(); });
// Clear collections so they are re-constructed with the new language
g_actions.clear();
g_wheel_actions.clear();
});
} }
KeyboardShortcuts::~KeyboardShortcuts() KeyboardShortcuts::~KeyboardShortcuts()
@ -1078,6 +542,12 @@ void KeyboardShortcuts::disableShortcut(const ui::Shortcut& shortcut,
// static // static
KeyContext KeyboardShortcuts::getCurrentKeyContext() 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(); auto* ctx = UIContext::instance();
Doc* doc = ctx->activeDocument(); Doc* doc = ctx->activeDocument();
if (doc && doc->isMaskVisible() && if (doc && doc->isMaskVisible() &&
@ -1309,34 +779,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 } // namespace app

View File

@ -85,8 +85,6 @@ private:
mutable Keys m_keys; mutable Keys m_keys;
}; };
std::string key_tooltip(const char* str, const Key* key);
inline std::string key_tooltip(const char* str, inline std::string key_tooltip(const char* str,
const char* commandName, const char* commandName,
const Params& params = Params(), const Params& params = Params(),

View File

@ -13,6 +13,7 @@
#include "app/app.h" #include "app/app.h"
#include "app/color.h" #include "app/color.h"
#include "app/color_spaces.h"
#include "app/color_utils.h" #include "app/color_utils.h"
#include "app/commands/commands.h" #include "app/commands/commands.h"
#include "app/modules/gfx.h" #include "app/modules/gfx.h"
@ -307,7 +308,8 @@ public:
if (tileImage) { if (tileImage) {
int w = tileImage->width(); int w = tileImage->width();
int h = tileImage->height(); 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());
convert_image_to_surface(tileImage.get(), convert_image_to_surface(tileImage.get(),
get_current_palette(), get_current_palette(),
surface.get(), surface.get(),

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2017 David Capello // Copyright (C) 2017 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -181,8 +181,7 @@ void SliceWindow::onPivotChange()
void SliceWindow::onToggleUserData() void SliceWindow::onToggleUserData()
{ {
m_userDataView.toggleVisibility(); m_userDataView.toggleVisibility();
remapWindow(); expandWindow(gfx::Size(bounds().w, sizeHint().h));
manager()->invalidate();
} }
void SliceWindow::onModifyField(ui::Entry* entry, const Mods mods) void SliceWindow::onModifyField(ui::Entry* entry, const Mods mods)

View File

@ -11,6 +11,7 @@
#include "app/ui/tabs.h" #include "app/ui/tabs.h"
#include "app/color_spaces.h"
#include "app/color_utils.h" #include "app/color_utils.h"
#include "app/modules/gfx.h" #include "app/modules/gfx.h"
#include "app/modules/gui.h" #include "app/modules/gui.h"
@ -969,7 +970,8 @@ void Tabs::createFloatingUILayer(Tab* tab)
ASSERT(!m_floatingUILayer); ASSERT(!m_floatingUILayer);
ui::Display* display = this->display(); 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());
// Fill the surface with pink color // Fill the surface with pink color
{ {

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello // Copyright (C) 2001-2016 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -132,8 +132,7 @@ void TagWindow::onRepeatChange()
void TagWindow::onToggleUserData() void TagWindow::onToggleUserData()
{ {
m_userDataView.toggleVisibility(); m_userDataView.toggleVisibility();
remapWindow(); expandWindow(gfx::Size(bounds().w, sizeHint().h));
manager()->invalidate();
} }
} // namespace app } // namespace app

View File

@ -4321,6 +4321,12 @@ void Timeline::clearAndInvalidateRange()
} }
} }
void Timeline::refresh()
{
regenerateRows();
invalidate();
}
app::gen::GlobalPref::Timeline& Timeline::timelinePref() const app::gen::GlobalPref::Timeline& Timeline::timelinePref() const
{ {
return Preferences::instance().timeline; return Preferences::instance().timeline;
@ -4545,7 +4551,7 @@ void Timeline::onDrag(ui::DragEvent& e)
m_range.clearRange(); m_range.clearRange();
setHot(hitTest(nullptr, e.position())); setHot(hitTest(nullptr, e.position()));
switch (m_hot.part) { switch (m_hot.part) {
case PART_NOTHING: invalidate(); case PART_NOTHING: invalidate(); [[fallthrough]];
case PART_ROW: case PART_ROW:
case PART_ROW_EYE_ICON: case PART_ROW_EYE_ICON:
case PART_ROW_CONTINUOUS_ICON: case PART_ROW_CONTINUOUS_ICON:
@ -4576,7 +4582,7 @@ void Timeline::onDrop(ui::DragEvent& e)
// Determine at which frame and layer the content was dropped on. // Determine at which frame and layer the content was dropped on.
frame_t frame = m_frame; frame_t frame = m_frame;
layer_t layerIndex = getLayerIndex(m_layer); layer_t layerIndex = m_sprite->root()->getLayerIndex(m_layer);
InsertionPoint insert = InsertionPoint::BeforeLayer; InsertionPoint insert = InsertionPoint::BeforeLayer;
DroppedOn droppedOn = DroppedOn::Unspecified; DroppedOn droppedOn = DroppedOn::Unspecified;
TRACE("m_dropRange.type() %d\n", m_dropRange.type()); TRACE("m_dropRange.type() %d\n", m_dropRange.type());
@ -4602,7 +4608,7 @@ void Timeline::onDrop(ui::DragEvent& e)
break; break;
case Range::kLayers: case Range::kLayers:
droppedOn = DroppedOn::Layer; 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(); auto* selectedLayer = *m_dropRange.selectedLayers().begin();
layerIndex = getLayerIndex(selectedLayer); layerIndex = getLayerIndex(selectedLayer);
} }

View File

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

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -23,6 +23,7 @@ class Image;
class Mask; class Mask;
class Palette; class Palette;
class PalettePicks; class PalettePicks;
class Slice;
class Tileset; class Tileset;
} // namespace doc } // namespace doc
@ -45,6 +46,7 @@ enum class ClipboardFormat {
PaletteEntries, PaletteEntries,
Tilemap, Tilemap,
Tileset, Tileset,
Slices,
}; };
class Clipboard : public ui::ClipboardDelegate { class Clipboard : public ui::ClipboardDelegate {
@ -74,6 +76,7 @@ public:
const doc::Palette* pal, const doc::Palette* pal,
const doc::Tileset* tileset); const doc::Tileset* tileset);
void copyPalette(const doc::Palette* palette, const doc::PalettePicks& picks); 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); void paste(Context* ctx, const bool interactive, const gfx::Point* position = nullptr);
doc::ImageRef getImage(doc::Palette* palette); doc::ImageRef getImage(doc::Palette* palette);
@ -106,6 +109,7 @@ private:
doc::Mask* mask, doc::Mask* mask,
doc::Palette* palette, doc::Palette* palette,
doc::Tileset* tileset, doc::Tileset* tileset,
const std::vector<doc::Slice*>* slices,
bool set_native_clipboard, bool set_native_clipboard,
bool image_source_is_transparent); bool image_source_is_transparent);
bool copyFromDocument(const Site& site, bool merged = false); 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.cpp
image_impl.cpp image_impl.cpp
image_io.cpp image_io.cpp
image_iterators2.cpp
layer.cpp layer.cpp
layer_io.cpp layer_io.cpp
layer_list.cpp layer_list.cpp

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // 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 // Copyright (c) 2001-2017 David Capello
// //
// This file is released under the terms of the MIT license. // 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); 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) static void set_sat(double& r, double& g, double& b, double s)
{ {
#undef MIN const double minv = std::min(std::min(r, g), b);
#undef MAX const double maxv = std::max(std::max(r, g), b);
#undef MID const double range = maxv - minv;
#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)))
// Fetch channel indices if (range > 0.0) {
const uint8_t imin = get_imin_channel(r, g, b); r = ((r - minv) * s) / range;
const uint8_t imax = get_imax_channel(r, g, b); g = ((g - minv) * s) / range;
const uint8_t imid = get_imid_channel(imin, imax); b = ((b - minv) * s) / range;
// 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;
} }
else else
mid = max = 0; r = g = b = 0.0;
min = 0;
} }
color_t rgba_blender_hsl_hue(color_t backdrop, color_t src, int opacity) 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 r = rgba_getr(backdrop) - rgba_getr(src);
int g = rgba_getg(backdrop) - rgba_getg(src); int g = rgba_getg(backdrop) - rgba_getg(src);
int b = rgba_getb(backdrop) - rgba_getb(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); return rgba_blender_normal(backdrop, src, opacity);
} }

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // 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 // Copyright (c) 2001-2016 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // 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 // Copyright (c) 2001-2016 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -12,6 +12,7 @@
#include "doc/color.h" #include "doc/color.h"
#include "doc/color_mode.h" #include "doc/color_mode.h"
#include "doc/image_buffer.h" #include "doc/image_buffer.h"
#include "doc/image_iterators2.h"
#include "doc/image_spec.h" #include "doc/image_spec.h"
#include "doc/object.h" #include "doc/object.h"
#include "doc/pixel_format.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 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; 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: protected:
Image(const ImageSpec& spec); 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 // 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<> template<>
inline void ImageImpl<IndexedTraits>::clear(color_t color) 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)); std::fill(p, p + rowBytes() * height(), (color ? 0xff : 0x00));
} }
#if DOC_USE_BITMAP_AS_1BPP
template<> template<>
inline color_t ImageImpl<BitmapTraits>::getPixel(int x, int y) const 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); 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); void copy_bitmaps(Image* dst, const Image* src, gfx::Clip area);
template<> template<>
inline void ImageImpl<BitmapTraits>::copy(const Image* src, gfx::Clip area) 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); copy_bitmaps(this, src, area);
} }
#endif // DOC_USE_BITMAP_AS_1BPP
} // namespace doc } // namespace doc
#endif #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 { class BitPixelAccess {
public: public:
@ -403,6 +405,8 @@ public:
} }
}; };
#endif // DOC_USE_BITMAP_AS_1BPP
} // namespace doc } // namespace doc
#endif #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 <gtest/gtest.h>
#include "doc/algorithm/random_image.h"
#include "doc/image.h" #include "doc/image.h"
#include "doc/primitives.h" #include "doc/primitives.h"
@ -25,9 +26,27 @@ protected:
ImageAllTypes() {} ImageAllTypes() {}
}; };
typedef testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits, BitmapTraits> ImageAllTraits; using ImageAllTraits = testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits, BitmapTraits>;
TYPED_TEST_SUITE(ImageAllTypes, ImageAllTraits); 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) TYPED_TEST(ImageAllTypes, PutGetAndIterators)
{ {
using ImageTraits = TypeParam; 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) int main(int argc, char** argv)
{ {
::testing::InitGoogleTest(&argc, 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; } static inline bool same_color(const pixel_t a, const pixel_t b) { return a == b; }
}; };
#if DOC_USE_BITMAP_AS_1BPP
struct BitmapTraits { struct BitmapTraits {
static const ColorMode color_mode = ColorMode::BITMAP; static const ColorMode color_mode = ColorMode::BITMAP;
static const PixelFormat pixel_format = IMAGE_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; } 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 { struct TilemapTraits {
static const ColorMode color_mode = ColorMode::TILEMAP; static const ColorMode color_mode = ColorMode::TILEMAP;
static const PixelFormat pixel_format = IMAGE_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->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 } // namespace doc

View File

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

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // Aseprite Document Library
// Copyright (c) 2020-2024 Igara Studio S.A. // Copyright (c) 2020-2025 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello // Copyright (c) 2001-2018 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -65,7 +65,7 @@ public:
void unfreeze(); void unfreeze();
// Returns true if the mask is frozen (See freeze/unfreeze functions). // Returns true if the mask is frozen (See freeze/unfreeze functions).
bool isFrozen() const { return m_freeze_count > 0; } bool isFrozen() const { return m_freezes > 0; }
// Returns true if the mask is a rectangular region. // Returns true if the mask is a rectangular region.
bool isRectangular() const; bool isRectangular() const;
@ -107,9 +107,7 @@ public:
void offsetOrigin(int dx, int dy); void offsetOrigin(int dx, int dy);
private: private:
void initialize(); int m_freezes = 0;
int m_freeze_count;
std::string m_name; // Mask name std::string m_name; // Mask name
gfx::Rect m_bounds; // Region bounds gfx::Rect m_bounds; // Region bounds
ImageRef m_bitmap; // Bitmapped image mask ImageRef m_bitmap; // Bitmapped image mask

View File

@ -9,6 +9,12 @@
#define DOC_PIXEL_FORMAT_H_INCLUDED #define DOC_PIXEL_FORMAT_H_INCLUDED
#pragma once #pragma once
// Use 1-bit per pixel in IMAGE_BITMAP
#define DOC_USE_BITMAP_AS_1BPP 1
// Use 8-bit per pixel in IMAGE_BITMAP
// #define DOC_USE_BITMAP_AS_1BPP 0
namespace doc { namespace doc {
// This enum might be replaced/deprecated/removed in the future, please use // This enum might be replaced/deprecated/removed in the future, please use

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // Aseprite Document Library
// Copyright (c) 2018-2023 Igara Studio S.A. // Copyright (c) 2018-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello // Copyright (c) 2001-2016 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -352,6 +352,18 @@ bool is_plain_image_templ(const Image* img, const color_t color)
return true; return true;
} }
template<typename ImageTraits>
bool is_color_used_templ(const Image* img, const doc::color_t color)
{
const LockImageBits<ImageTraits> bits(img);
auto it = bits.begin(), end = bits.end();
for (; it != end; ++it) {
if (*it == color)
return true;
}
return false;
}
template<typename ImageTraits> template<typename ImageTraits>
int count_diff_between_images_templ(const Image* i1, const Image* i2) int count_diff_between_images_templ(const Image* i1, const Image* i2)
{ {
@ -464,6 +476,16 @@ bool is_plain_image(const Image* img, color_t c)
return false; return false;
} }
bool is_color_used(const Image* img, color_t c)
{
ASSERT(img->pixelFormat() == IMAGE_RGB || img->pixelFormat() == IMAGE_GRAYSCALE);
switch (img->pixelFormat()) {
case IMAGE_RGB: return is_color_used_templ<RgbTraits>(img, c);
case IMAGE_GRAYSCALE: return is_color_used_templ<GrayscaleTraits>(img, c);
}
return false;
}
bool is_empty_image(const Image* img) bool is_empty_image(const Image* img)
{ {
color_t c = 0; // alpha = 0 color_t c = 0; // alpha = 0

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // Aseprite Document Library
// Copyright (c) 2018-2023 Igara Studio S.A. // Copyright (c) 2018-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello // Copyright (c) 2001-2016 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -65,6 +65,7 @@ void fill_ellipse(Image* image,
color_t color); color_t color);
bool is_plain_image(const Image* img, color_t c); bool is_plain_image(const Image* img, color_t c);
bool is_color_used(const Image* img, color_t c);
bool is_empty_image(const Image* img); bool is_empty_image(const Image* img);
int count_diff_between_images(const Image* i1, const Image* i2); int count_diff_between_images(const Image* i1, const Image* i2);

View File

@ -49,6 +49,8 @@ inline void put_pixel_fast(Image* image, int x, int y, typename Traits::pixel_t
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Bitmap specialization // Bitmap specialization
#if DOC_USE_BITMAP_AS_1BPP
template<> template<>
inline BitmapTraits::pixel_t get_pixel_fast<BitmapTraits>(const Image* image, int x, int y) inline BitmapTraits::pixel_t get_pixel_fast<BitmapTraits>(const Image* image, int x, int y)
{ {
@ -70,6 +72,8 @@ inline void put_pixel_fast<BitmapTraits>(Image* image, int x, int y, BitmapTrait
*image->getPixelAddress(x, y) &= ~(1 << (x % 8)); *image->getPixelAddress(x, y) &= ~(1 << (x % 8));
} }
#endif // DOC_USE_BITMAP_AS_1BPP
} // namespace doc } // namespace doc
#endif #endif

View File

@ -31,7 +31,10 @@ Slices::~Slices()
void Slices::add(Slice* slice) void Slices::add(Slice* slice)
{ {
m_slices.push_back(slice); // Insert the slice at the begining to display it at the front of the others.
// This is useful when duplicating (or copy & pasting) slices, because the
// user can drag the new slices instead of the originally selected ones.
m_slices.insert(m_slices.begin(), slice);
slice->setOwner(this); slice->setOwner(this);
} }

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // Aseprite Document Library
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -223,6 +223,18 @@ bool Sprite::isOpaque() const
return (bg && bg->isVisible()); return (bg && bg->isVisible());
} }
bool Sprite::isColorUsed(const doc::color_t c) const
{
ASSERT(pixelFormat() == IMAGE_RGB || pixelFormat() == IMAGE_GRAYSCALE);
for (Cel* cel : cels()) {
if (cel && cel->image()) {
if (is_color_used(cel->image(), c))
return true;
}
}
return false;
}
bool Sprite::needAlpha() const bool Sprite::needAlpha() const
{ {
switch (pixelFormat()) { switch (pixelFormat()) {

View File

@ -110,6 +110,9 @@ public:
// Returns true if the sprite has a background layer and it's visible // Returns true if the sprite has a background layer and it's visible
bool isOpaque() const; bool isOpaque() const;
// Returns true if the sprite is using a pixel with color c
bool isColorUsed(const doc::color_t c) const;
// Returns true if the rendered images will contain alpha values less // Returns true if the rendered images will contain alpha values less
// than 255. Only RGBA and Grayscale images without background needs // than 255. Only RGBA and Grayscale images without background needs
// alpha channel in the render. // alpha channel in the render.

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

86
src/main/osx/Info.plist Normal file
View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>aseprite</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>Document.icns</string>
<key>CFBundleTypeName</key>
<string>Aseprite Sprite</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>ase</string>
<string>bmp</string>
<string>flc</string>
<string>fli</string>
<string>gif</string>
<string>ico</string>
<string>jpeg</string>
<string>jpg</string>
<string>pcx</string>
<string>png</string>
<string>tga</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>Document.icns</string>
<key>CFBundleTypeName</key>
<string>Aseprite Sprite</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>aseprite-extension</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>Extension.icns</string>
<key>CFBundleTypeName</key>
<string>Aseprite Extension</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
</array>
<key>CFBundleDisplayName</key>
<string>Aseprite</string>
<key>CFBundleExecutable</key>
<string>aseprite</string>
<key>CFBundleIdentifier</key>
<string>org.aseprite.Aseprite</string>
<key>CFBundleName</key>
<string>Aseprite</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.3</string>
<key>CFBundleVersion</key>
<string>1.3</string>
<key>CFBundleIconFile</key>
<string>Aseprite.icns</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.graphics-design</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2001-2025, Igara Studio S.A.
All rights reserved.</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
</dict>
</plist>

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // Aseprite UI Library
// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2019-2025 Igara Studio S.A.
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information. // Read LICENSE.txt for more information.
@ -87,7 +87,8 @@ void Display::configureBackLayer()
layerSurface->width() != displaySurface->width() || layerSurface->width() != displaySurface->width() ||
layerSurface->height() != displaySurface->height()) { layerSurface->height() != displaySurface->height()) {
layerSurface = os::System::instance()->makeSurface(displaySurface->width(), layerSurface = os::System::instance()->makeSurface(displaySurface->width(),
displaySurface->height()); displaySurface->height(),
displaySurface->colorSpace());
layer->setSurface(layerSurface); layer->setSurface(layerSurface);
} }
} }

View File

@ -128,6 +128,15 @@ int Entry::lastCaretPos() const
return int(m_boxes.size() - 1); return int(m_boxes.size() - 1);
} }
gfx::Point Entry::caretPosOnScreen() const
{
const gfx::Point caretPos = getCharBoxBounds(m_caret).point2();
const os::Window* nativeWindow = display()->nativeWindow();
const gfx::Point pos = nativeWindow->pointToScreen(caretPos + bounds().origin());
return pos;
}
void Entry::setCaretPos(const int pos) void Entry::setCaretPos(const int pos)
{ {
gfx::Size caretSize = theme()->getEntryCaretSize(this); gfx::Size caretSize = theme()->getEntryCaretSize(this);
@ -160,6 +169,8 @@ void Entry::setCaretPos(const int pos)
startTimer(); startTimer();
m_state = true; m_state = true;
os::System::instance()->setTextInput(true, caretPosOnScreen());
invalidate(); invalidate();
} }
@ -251,7 +262,7 @@ gfx::Rect Entry::getEntryTextBounds() const
return onGetEntryTextBounds(); return onGetEntryTextBounds();
} }
gfx::Rect Entry::getCharBoxBounds(const int i) gfx::Rect Entry::getCharBoxBounds(const int i) const
{ {
ASSERT(i >= 0 && i < int(m_boxes.size())); ASSERT(i >= 0 && i < int(m_boxes.size()));
if (i >= 0 && i < int(m_boxes.size())) if (i >= 0 && i < int(m_boxes.size()))
@ -288,8 +299,9 @@ bool Entry::onProcessMessage(Message* msg)
} }
// Start processing dead keys // Start processing dead keys
if (m_translate_dead_keys) if (m_translate_dead_keys) {
os::System::instance()->setTextInput(true); os::System::instance()->setTextInput(true, caretPosOnScreen());
}
break; break;
case kFocusLeaveMessage: case kFocusLeaveMessage:

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