Compare commits

...

134 Commits

Author SHA1 Message Date
Christian Kaiser 846c9cad7c Update to use setTextInput (maybe?) 2025-08-02 00:17:45 -03:00
Christian Kaiser 0998bec0e5 Move colors to theme, style fixes, invalidate on font change 2025-08-02 00:02:24 -03:00
Christian Kaiser dbd2172c73 *WIP* TextEdit implementation 2025-08-02 00:02:23 -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
David Capello 2ba051b59b Fix crash deselecting moved pixels when using certain extensions (fix #5280)
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
Although the issue refers to deselecting MovingPixelsState, the same
crash could happen when canceling/finishing WritingTextState or
MovingSelectionState. This fixes the crash for all these states.
2025-07-23 19:01:37 -03:00
Christian Kaiser 3fcb000eb1 Fix slice transformations not updating editors
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-22 02:11:07 -03:00
David Capello af9dc3c817 Fix brush boundaries accumulation switching brush type only (fix #5281)
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
2025-07-21 17:28:11 -03:00
David Capello 250dfdc86a Convert the brush generation counter into an atomic var 2025-07-21 17:27:23 -03:00
David Capello c904c41b39 Don't show stroke/fill option for theme fonts
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-18 18:48:35 -03:00
David Capello 9e941e9a8b [win32] Fix listing hidden files on Windows (related to #5269 / #3079) 2025-07-18 18:37:44 -03:00
Liebranca bbab4d5875 Add 'Show hidden' check to file selector 2025-07-18 18:26:35 -03:00
David Capello 5c4daff128 Add options to stroke/fill text (fix #5271)
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-14 23:17:25 -03:00
David Capello 11a7b061ff Remove unused var 2025-07-14 20:24:32 -03:00
David Capello 283bedf77e Add pinned/recent folders to export file popup menus
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
Related to https://community.aseprite.org/t/25920
2025-07-10 18:55:28 -03:00
David Capello 2eeb6f04a7 Fix "buttononly" bool attribute for <filename> widget 2025-07-10 18:13:10 -03:00
David Capello 706d0b8a7a Add possibility to select export file names w/"one click"
It's not with one click, it requires dragging the mouse but it's
better than two clicks.

Related to https://community.aseprite.org/t/25920
2025-07-10 18:12:38 -03:00
David Capello 2f3a7f5dec Update to new laf API: setTranslateDeadKeys -> setTextInput
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
2025-07-10 16:06:03 -03:00
David Capello d5de74b715 Update laf module
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
2025-07-10 08:52:01 -03:00
David Capello 2d87a7b184 [ui] Fix drawing cursor at the end of the Entry/Combobox field
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
Regression introduced in 1a6a39700e
2025-07-07 22:21:58 -03:00
Gaspar Capello 2f22804fe8 Fix GIF animations with semi-transparent colors (fix #5226)
Prior to this fix, animations were incorrectly converted to GIF when
the animation contained semi-transparent colors or backgrounds
(specifically, colors with alpha <= 128). Essentially, semi-transparent
pixels didn't repaint the pixel of the processed frame, meaning they
retained the color from the previous frame.
2025-07-07 17:43:19 -03:00
Gaspar Capello bf1b4c6f50 Fix session 'isRunning' detection (fix #5252)
Before this fix, sometimes files available for recovery aren't
displayed due to the coincidence of the pid number of a crashed
session with the current pid number of the current session. This
coincidence caused Aseprite to falsely detect that it was
the current session.
2025-07-07 17:15:36 -03:00
David Capello 250244c777 [ui] Fix possible crash when there is no TextBlob to paint (fix #5255)
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
2025-07-07 12:39:12 -03:00
David Capello b4555fc098 Add "Initial State" string for "Undo History" window to en.ini
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-03 11:18:21 -03:00
Gaspar Capello 8783135bf7 Fix convert_image_to_surface function (fix #5257)
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
Before this fix, 'convert_image_to_surface' would generate saturated
colors when converting semi-transparent colors. This happened because
 Skia Surfaces assumed colors with pre-multiplied alpha.
2025-07-02 20:44:33 -03:00
David Capello 1a6a39700e Fix suffix separation in Entry fields w/TTF fonts (fix #5261) 2025-07-02 20:43:17 -03:00
David Capello ce742bcbc1 Fix typo in GitHub profile URL
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
2025-07-01 21:44:51 -03:00
David Capello 3c350c3e67 Update laf module
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-06-25 08:20:50 -03:00
David Capello 220d2d3a2d Update laf module
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-06-24 11:16:06 -03:00
Martín Capello 322040c4fb Recover Windows Vista compatibility (fix #5211) 2025-06-24 10:10:55 -03:00
David Capello ee69a2f2f6 Fix baseline alignment of file list items/names w/multiple fonts/emojis
build-auto / build-auto (Debug, macos-latest) (push) Waiting to run Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Waiting to run Details
build-auto / build-auto (Debug, windows-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Waiting to run Details
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
2025-06-23 23:03:16 -03:00
David Capello 91f8410749 Fix bug in news items when multiple fonts are needed (fix #5221) 2025-06-23 22:49:05 -03:00
Gaspar Capello 8d5534d4eb Fix do not move the pivot when dragging a selection with Alt key to snap to grid (fix #5238) 2025-06-23 18:54:32 -03:00
David Capello 2ee8c68d94 Update laf module (fix #5240) 2025-06-23 17:56:38 -03:00
David Capello f7040190cc Fix implicit conversion of nullptr constant to 'bool'
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
2025-06-23 09:12:36 -03:00
David Capello 68bf0839aa Don't use userConfigPath to output automated test files 2025-06-23 09:08:02 -03:00
Necronomison e5917389cb
Export user & extension defined properties (fix #5186, #5187) 2025-06-23 09:06:05 -03:00
LeSnow-Ye 27eecb3bdc Greedy merge pixels on saving to SVG formats
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-06-20 10:15:06 -03:00
David Capello b6cbeefb6e [lua] Update scripting API version to 34 2025-06-20 10:00:33 -03:00
Christian Kaiser 5596c3270d Add blendMode, name, opacity and visibility Sprite layer events 2025-06-20 09:58:09 -03:00
David Capello ab29b84f25 Fix crash accessing null extra cel when rendering interactive text (fix #5227)
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-06-20 09:51:04 -03:00
David Capello 444ef0f6b4 Update laf (fix #5233)
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
2025-06-19 19:38:50 -03:00
David Capello b3b956516b Fix themes loading custom TTF fonts without specifying a size (fix #5229)
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-06-13 18:22:00 -03:00
David Capello 32bf699655 Mitigate Linux/X11 lag when resizing windows (#5028)
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
In relation to these findings:
https://github.com/aseprite/aseprite/pull/5166#issuecomment-2944585171
https://github.com/aseprite/aseprite/pull/5166#issuecomment-2945369914
2025-06-11 17:30:00 -03:00
David Capello 5ceeac2f26 Add possibility to disable hinting for theme ttf/otf fonts (fix #5215) 2025-06-11 17:20:17 -03:00
David Capello 0ddf7d939b [ui] Align text with different fonts correctly to ui::Entry baseline
Regression introduced in af6e8b65c3
2025-06-11 17:06:55 -03:00
David Capello bd13e5d574 Fix bug w/user themes with odd size value for the Aseprite font 2025-06-11 15:45:23 -03:00
David Capello e53aa99080 Possible fix to crashes using the FontEntry widget (fix #5214) 2025-06-11 15:42:03 -03:00
David Capello af6e8b65c3 Fix default font for CJK and other translations (fix #5210)
Now we use the correct fallback font with its size for non-existent
glyph in the sprite sheet font.
2025-06-11 14:40:56 -03:00
Martín Capello 68342bdb66 Call parent's class onOpen (fix #5209)
This makes the window's Open signal to be triggered as usual
2025-06-11 12:57:07 -03:00
David Capello 8ff208d8d5 Fix warnings about unused var
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
2025-06-11 10:58:12 -03:00
David Capello 2bc4f0582d Fix crash (and show error message) when default font is nullptr (fix #5208)
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
Probably an edge-case for users that are creating new themes.
2025-06-10 22:02:12 -03:00
David Capello 2be4dc1474 Revert part of "Startup optimizations (#5090)" (fix #5207)
This reverts part of commit b130601716.
2025-06-10 21:19:37 -03:00
David Capello 2b522e222b Rename ui::Accelerator to ui::Shortcut
This was a pending refactor, where "user.aseprite-keys" files were
already using the "shortcut" attribute (instead of "accelerator").
This will include a refactor in the Weblate projects/all translations
to change [select_accelerator] section to [select_shortcut]. But that
must be coordinated after this commit is merged.
2025-06-09 17:20:16 -03:00
Maplegecko 698d79b049 [lua] Make Dialog:separator more in line with other widgets (fix #4989) 2025-06-09 13:26:39 -03:00
David Capello 03422e7251 [lua] Update scripting API version to 33 2025-06-07 12:08:00 -03:00
David Capello 3f1f131a39 Update laf 2025-06-07 12:05:56 -03:00
Christian Kaiser 54ea61fe58 Re-add os.rename and os.remove support 2025-06-07 12:05:34 -03:00
JoshuaL03 4c31c950c5
Fix file dialog size not being loaded properly (fix #4460) (#5202) 2025-06-07 11:30:27 -03:00
martincapello afbd28b3b4 Improve panel resizing performance
By avoiding calculating bounds for HIDDEN panel's children.
2025-06-05 11:11:20 -03:00
Joshua Lopez 1519589184 Fix user data visibility after window size reset (fix aseprite#4923) 2025-06-04 13:37:38 -03:00
Luke Barcenas 2c917e30c0 Fix: Prevent scrollbar from shifting on click 2025-06-03 16:54:42 -03:00
Gaspar Capello 5d0214a89d Fix wrong export of tileset (fix #5053)
Before this fix, when a sprite's color mode was set to Grayscale and
a tileset was exported, the resulting sheet was full of broken
tile images due to a misinterpretation of the pixel format in
the samples.

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

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

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

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

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

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

Pixel ratios other than 1:1 are now considered in the drawing logic of
diagonal axes.
2025-04-21 19:20:43 -03:00
David Capello 2e37ac9b83 [build] Don't use unsupported ${var,,} syntax on some shells (fix #5082) 2025-04-18 22:43:36 -03:00
David Capello 753d892af2 [build] read -N 1 is not supported on macOS bash 2025-04-18 22:43:18 -03:00
David Capello 8a8ddbc630 Improve performance loading list of fonts using an app::Task
We list the fonts in a background thread to fill the list of fonts in
the UI. And now we are inserting the fonts in alphabetical order.
2025-04-18 19:56:56 -03:00
David Capello 943c3b28df Merge branch 'main' into beta 2025-04-17 21:10:04 -03:00
David Capello d9a138357e Update modules 2025-04-17 20:34:07 -03:00
David Capello 978000a9dd Better support to use system fonts from themes
* There is a new system="" attribute for <font> elements
* New <font><windows ... /><macos ... /><linux /></font> elements to
  select platform specific fonts
* Fixed several bugs related to re-using fonts from the cache when
  customizing the current theme font from Edit > Preferences > Theme
* Removed Fonts::infoFromFont() adding FontInfo(FontData) constructor
2025-04-17 19:43:32 -03:00
David Capello b3f4e37b69 Add option to switch font hinting (fix #4931, aseprite/laf#138) 2025-04-11 17:26:03 -03:00
David Capello d8632b6208 Move misc/ scripts to laf library 2025-04-11 09:03:28 -03:00
David Capello 3c4d012210 [ci] Fix slow tests 2025-04-10 09:56:46 -03:00
David Capello d51a6d4f51 [ci] New build-auto job to test the build.sh script
This runs only when the build.sh (or some misc/ script) is modified.
2025-04-10 09:21:06 -03:00
David Capello 2c9eb2a801 New misc/skia-url.sh script to simplify downloading the required Skia branch 2025-04-10 09:08:06 -03:00
David Capello bae8520580 Update modules due cmake_minimum_required() issues (#5087, #5071) 2025-04-02 21:44:03 -03:00
David Capello 7167969963 [theme] Fix horizontal separator alignment 2025-04-02 09:32:34 -03:00
David Capello 537ccd393f Prevent polling keyboard state for each created ui::Message
Getting the keyboard state to fill the keyboard modifiers can be
expensive (mainly on Windows calling GetAsyncKeyState). So we can lazy
evaluate the modifiers when they are needed.
2025-04-02 08:00:11 -03:00
Christian Kaiser b130601716
Startup optimizations (#5090)
* Delay DitheringSelector startup
* Delay setting the drag target
* Delay & enqueue menu reloading
2025-04-02 07:59:52 -03:00
Martín Capello c68b4923f8 Prevent using an invalid drawing point (fix #5055) 2025-03-12 15:47:23 -03:00
David Capello fc63532fef Ignore .vscode directory 2025-03-03 11:24:52 -03:00
221 changed files with 5846 additions and 2392 deletions

View File

@ -60,6 +60,8 @@ Checks: >
-readability-uppercase-literal-suffix
WarningsAsErrors: ''
CheckOptions:
- key: readability-implicit-bool-conversion.AllowIntegerConditions
value: true
- key: readability-implicit-bool-conversion.AllowPointerConditions
value: true
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic

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

@ -0,0 +1,43 @@
name: build-auto
on:
push:
paths:
- '.github/workflows/build-auto.yml'
- 'build.sh'
- 'laf'
jobs:
build-auto:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
build_type: [RelWithDebInfo, Debug]
steps:
- uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Install Dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y \
libpixman-1-dev libfreetype6-dev libharfbuzz-dev zlib1g-dev \
libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
- uses: aseprite/get-ninja@main
- uses: ilammy/msvc-dev-cmd@v1
if: runner.os == 'Windows'
- name: Building
shell: bash
run: |
bash build.sh --auto --norun
- name: Running CLI Tests
shell: bash
run: |
if [[ "${{ runner.os }}" == "Linux" ]] ; then
export XVFB=xvfb-run
fi
export ASEPRITE=$PWD/build/bin/aseprite
cd tests
$XVFB bash run-tests.sh

View File

@ -34,16 +34,14 @@ jobs:
shell: bash
run: |
if [[ "${{ runner.os }}" == "Windows" ]] ; then
choco install wget -y --no-progress
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-Windows-Release-x64.zip
unzip Skia-Windows-Release-x64.zip -d skia
elif [[ "${{ runner.os }}" == "macOS" ]] ; then
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-macOS-Release-arm64.zip
unzip Skia-macOS-Release-arm64.zip -d skia
this_dir=$(cygpath "${{ github.workspace }}")
else
wget https://github.com/aseprite/skia/releases/download/m124-08a5439a6b/Skia-Linux-Release-x64.zip
unzip Skia-Linux-Release-x64.zip -d skia
this_dir="${{ github.workspace }}"
fi
skia_url=$(source $this_dir/laf/misc/skia-url.sh | xargs)
skia_file=$(basename $skia_url)
curl --ssl-revoke-best-effort -L -o "$skia_file" "$skia_url"
unzip "$skia_file" -d skia
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.17
if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }}

View File

@ -13,6 +13,6 @@ jobs:
post-comments:
runs-on: ubuntu-latest
steps:
- uses: ZedThree/clang-tidy-review/post@v0.20.1
- uses: ZedThree/clang-tidy-review/post@v0.21.0
with:
token: ${{ secrets.CLANG_TIDY_TOKEN }}

2
.gitignore vendored
View File

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

98
AUTHORS.md Normal file
View File

@ -0,0 +1,98 @@
# Credits
Aseprite is being developed and maintained currently by [Igara Studio](https://igara.com/).
The active team of developers is:
* [David Capello](https://github.com/dacap)
* [Gaspar Capello](https://github.com/Gasparoken)
* [Martín Capello](https://github.com/martincapello)
* [Christian Kaiser](https://github.com/ckaiser)
* [Dante Paola](https://github.com/Liebranca)
Previous team members that contributed with code/docs/scripts/graphics:
* [Kacper Woźniak](https://github.com/thkwznk)
* [Joshua Ogunyinka](https://github.com/iamOgunyinka)
* [David Campo](https://github.com/dncampo)
## Translations
The translation work of Aseprite is only possible thanks to the
contribution, help, and good will of several translators coordinated
through our Weblate project:
* [Translation Credits](strings/README.md)
## Graphics
Aseprite logo was created by David Capello. Graphics used as background
of [Aseprite home page](https://www.aseprite.org),
on [Steam Store](https://store.steampowered.com/app/431730/Aseprite/),
and [social media channels](https://bsky.app/profile/aseprite.org),
were created by:
* [Ilija Melentijevic](https://ilkke.net/)
## Themes
The default Aseprite font was created by David Capello, and the
default Aseprite theme was introduced in v0.8, originally created by:
* Ilija Melentijevic
A modified dark version of this theme was introduced in v1.3-beta1, created by:
* [Nicolas Desilets](https://twitter.com/MapleGecko)
These themes are now being maintained by Igara Studio and external
contributors from time to time.
## Palettes
Aseprite includes color palettes created by:
* [Richard "DawnBringer" Fhager](http://pixeljoint.com/p/23821.htm), [DB16](http://pixeljoint.com/forum/forum_posts.asp?TID=12795), [DB32](http://pixeljoint.com/forum/forum_posts.asp?TID=16247) (default Aseprite color palette)
* [Arne Niklas Jansson](http://androidarts.com/), [16 colors](http://androidarts.com/palette/16pal.htm), [32 colors](http://wayofthepixel.net/index.php?topic=15824.msg144494)
* [ENDESGA Studios](https://twitter.com/ENDESGA), [EDG16 and EDG32](https://forums.tigsource.com/index.php?topic=46126.msg1279124#msg1279124), and [other palettes](https://twitter.com/ENDESGA/status/865812366931353600)
* [Hyohnoo Games](https://twitter.com/Hyohnoo), [mail24](https://twitter.com/Hyohnoo/status/797472587974639616) palette
* [Davit Masia](https://twitter.com/DavitMasia), [matriax8c](https://twitter.com/DavitMasia/status/834862452164612096) palette
* [Javier Guerrero](https://twitter.com/Xavier_Gd), [nyx8](https://twitter.com/Xavier_Gd/status/868519467864686594) palette
* [Adigun A. Polack](https://twitter.com/adigunpolack), [AAP-64](http://pixeljoint.com/pixelart/119466.htm), [AAP-Splendor128](http://pixeljoint.com/pixelart/120714.htm), [SimpleJPC-16](http://pixeljoint.com/pixelart/119844.htm), and [AAP-Micro12](http://pixeljoint.com/pixelart/121151.htm) palette
* [PineTreePizza](https://twitter.com/PineTreePizza), [Rosy-42](https://twitter.com/PineTreePizza/status/1006536191955623938) palette
## Pixel-art Features
Aseprite tries to replicate some pixel-art algorithms:
* [Shading Ink](https://aseprite.org/docs/shading/): created as a simplification of GrafX2 shade mode, thanks to Ilija Melentijevic for introducing me to this feature in 2009
* [RotSprite](http://forums.sonicretro.org/index.php?showtopic=8848&st=15&p=159754&#entry159754) by Xenowhirl.
* [Pixel perfect drawing algorithm](https://deepnight.net/blog/tools/pixel-perfect-drawing/)
by [Sébastien Bénard](https://twitter.com/deepnightfr) and
[Carduus](https://twitter.com/CarduusHimself/status/420554200737935361).
## Community
A special thanks to @Outlander for helping us moderating our [Discord server](https://discord.gg/Yb2CeX8).
Thanks to all the people that hung around for such a long time.
## Contributors
Thank you everyone who contributed to Aseprite with ideas, patches,
code, bug reports, new features, donations, tutorials, videos,
personal messages, chats, emails, tweets, posts, questions, libraries,
compilers, and any other tools that made this program possible today.
* Thanks to all [contributors](https://github.com/aseprite/aseprite/graphs/contributors)
* Thanks to all developers and maintainers behind [other open source projects](docs/LICENSES.md) used by Aseprite
* Thanks to all early PayPal donors and donors from our Pledgie Campaign (before Aseprite was commercialized)
* Thanks to every who support our business model: this source-available / sell-binaries combo
* Thanks to schools and [educational institutions](https://aseprite.org/educational)
that are using Aseprite in their classrooms <3
* Thanks to our family and friends who always support our work
It's been more years than I can remember, sorry if we missed someone,
please drop me a line to [david@igara.com](mailto:david@igara.com) to
fix something or say hi. We'll try to keep this updated (for past and
future contributors).
Sincerely, David.

View File

@ -243,7 +243,13 @@ if(USE_SHARED_LIBPNG)
add_definitions(${PNG_DEFINITIONS})
else()
set(PNG_FOUND ON)
set(PNG_LIBRARY png_static)
# Skia on Linux includes libpng symbols
if(UNIX AND NOT APPLE AND LAF_BACKEND STREQUAL "skia")
set(PNG_LIBRARY skia)
else()
set(PNG_LIBRARY png_static)
endif()
set(PNG_LIBRARIES ${PNG_LIBRARY})
set(PNG_INCLUDE_DIRS
${LIBPNG_DIR}

View File

@ -6,7 +6,8 @@
* [Windows dependencies](#windows-dependencies)
* [macOS dependencies](#macos-dependencies)
* [Linux dependencies](#linux-dependencies)
* [Compiling](#compiling)
* [Automatic Building](#automatic-building)
* [Manual Building](#manual-building)
* [Windows details](#windows-details)
* [MinGW](#mingw)
* [macOS details](#macos-details)
@ -17,11 +18,12 @@
# Platforms
You should be able to compile Aseprite successfully on the following
platforms:
platforms (older and newer versions might work):
* Windows 11 + [Visual Studio Community 2022 + Windows 10.0 SDK (the latest version available)](https://imgur.com/a/7zs51IT) (we don't support [MinGW](#mingw))
* macOS 13.0.1 Ventura + Xcode 14.1 + macOS 11.3 SDK (older version might work)
* Linux Ubuntu Bionic 18.04 + clang 10.0
* Windows 11 + [Visual Studio Community 2022 + Windows 11 SDK](https://imgur.com/a/7zs51IT)
* *Important*: We don't support [MinGW](#mingw)
* macOS 15.2 Sequoia + Xcode 16.3 + macOS 15.4 SDK
* Linux Ubuntu Focal Fossa 20.04 + clang 12
# Get the source code
@ -49,7 +51,7 @@ clone the repository on Windows.
To compile Aseprite you will need:
* The latest version of [CMake](https://cmake.org) (3.16 or greater)
* The latest version of [CMake](https://cmake.org)
* [Ninja](https://ninja-build.org) build system
* And a compiled version of the `aseprite-m124` branch of
the [Skia library](https://github.com/aseprite/skia#readme).
@ -59,25 +61,24 @@ To compile Aseprite you will need:
## Windows dependencies
* Windows 10/11 (we don't support cross-compiling)
* Windows 11 (we don't support cross-compiling)
* [Visual Studio Community 2022](https://visualstudio.microsoft.com/downloads/) (we don't support [MinGW](#mingw))
* The [Desktop development with C++ item + Windows 10.0.18362.0 SDK](https://imgur.com/a/7zs51IT)
from the Visual Studio installer
* The [Desktop development with C++ item + Windows 10.0.26100.0 SDK](https://imgur.com/a/7zs51IT)
from Visual Studio installer
## macOS dependencies
On macOS you will need macOS 11.3 SDK and Xcode 13.1 (older versions
might work).
On macOS you will need macOS 15.4 SDK and Xcode 16.3 (older versions might work).
## Linux dependencies
You will need the following dependencies on Ubuntu/Debian:
sudo apt-get install -y g++ clang libc++-dev libc++abi-dev cmake ninja-build libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
sudo apt-get install -y g++ clang cmake ninja-build libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
Or use clang-10 packages (or newer) in case that clang in your distribution is older than clang 10.0:
Or use clang-12 packages (or newer) in case that clang in your distribution is older than clang 12.0:
sudo apt-get install -y clang-10 libc++-10-dev libc++abi-10-dev
sudo apt-get install -y clang-12
On Fedora:
@ -85,13 +86,24 @@ On Fedora:
On Arch:
sudo pacman -S gcc clang libc++ cmake ninja libx11 libxcursor mesa-libgl fontconfig libwebp
sudo pacman -S gcc clang cmake ninja libx11 libxcursor mesa-libgl fontconfig libwebp
On SUSE:
sudo zypper install gcc-c++ clang libc++-devel libc++abi-devel cmake ninja libX11-devel libXcursor-devel libXi-devel Mesa-libGL-devel fontconfig-devel
sudo zypper install gcc-c++ clang cmake ninja libX11-devel libXcursor-devel libXi-devel Mesa-libGL-devel fontconfig-devel
# Compiling
# Automatic Building
We offer a new [build script](build.sh) that automates and help you to
compile Aseprite following instructions on screen. This will be the
preferred method for new users and developers to compile Aseprite.
After you get [get Aseprite code](#get-the-source-code) and install
[its dependencies](#dependencies), you can run [build.cmd](build.cmd)
file on Windows double-clicking it, or [build.sh](build.sh) on macOS or
Linux running it from the terminal from the same Aseprite folder.
# Manual Building
1. [Get Aseprite code](#get-the-source-code), put it in a folder like
`C:\aseprite`, and create a `build` directory inside to leave all
@ -223,7 +235,9 @@ If you have a Retina display, check the following issue:
## Linux details
You need to use clang and libc++ to compile Aseprite:
You can compile Aseprite with gcc or clang. In case that you are using
the [pre-compiled Skia version](https://github.com/aseprite/skia/releases/),
you must use libstdc++ to compile Aseprite:
cd aseprite
mkdir build
@ -232,8 +246,8 @@ You need to use clang and libc++ to compile Aseprite:
export CXX=clang++
cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_FLAGS:STRING=-stdlib=libc++ \
-DCMAKE_EXE_LINKER_FLAGS:STRING=-stdlib=libc++ \
-DCMAKE_CXX_FLAGS:STRING=-stdlib=libstdc++ \
-DCMAKE_EXE_LINKER_FLAGS:STRING=-stdlib=libstdc++ \
-DLAF_BACKEND=skia \
-DSKIA_DIR=$HOME/deps/skia \
-DSKIA_LIBRARY_DIR=$HOME/deps/skia/out/Release-x64 \
@ -245,13 +259,6 @@ You need to use clang and libc++ to compile Aseprite:
In this case, `$HOME/deps/skia` is the directory where Skia was
compiled or uncompressed.
### GCC compiler
In case that you are using the pre-compiled Skia version, you must use
the clang compiler and libc++ to compile Aseprite. Only if you compile
Skia with GCC, you will be able to compile Aseprite with GCC, and this
is not recommended as you will have a performance penalty doing so.
# Using shared third party libraries
If you don't want to use the embedded code of third party libraries

View File

@ -48,45 +48,14 @@ You can ask for help in:
[YouTube](https://www.youtube.com/user/aseprite),
[Instagram](https://www.instagram.com/aseprite/).
## Authors
Aseprite is being developed by [Igara Studio](https://igara.com/):
* [David Capello](https://davidcapello.com/)
* [Gaspar Capello](https://github.com/Gasparoken)
* [Martín Capello](https://github.com/martincapello)
## Credits
The default Aseprite theme was introduced in v0.8, created by:
Aseprite was originally created by [David Capello](https://davidcapello.com/)
and is now being developed and maintained by [Igara Studio](https://igara.com/)
and contributors.
* [Ilija Melentijevic](https://ilkke.net/)
A modified dark version of this theme introduced in v1.3-beta1 was created by:
* [Nicolas Desilets](https://twitter.com/MapleGecko)
* [David Capello](https://twitter.com/davidcapello)
Aseprite includes color palettes created by:
* [Richard "DawnBringer" Fhager](http://pixeljoint.com/p/23821.htm), [16 colors](http://pixeljoint.com/forum/forum_posts.asp?TID=12795), [32 colors](http://pixeljoint.com/forum/forum_posts.asp?TID=16247).
* [Arne Niklas Jansson](http://androidarts.com/), [16 colors](http://androidarts.com/palette/16pal.htm), [32 colors](http://wayofthepixel.net/index.php?topic=15824.msg144494).
* [ENDESGA Studios](https://twitter.com/ENDESGA), [EDG16 and EDG32](https://forums.tigsource.com/index.php?topic=46126.msg1279124#msg1279124), and [other palettes](https://twitter.com/ENDESGA/status/865812366931353600).
* [Hyohnoo Games](https://twitter.com/Hyohnoo), [mail24](https://twitter.com/Hyohnoo/status/797472587974639616) palette.
* [Davit Masia](https://twitter.com/DavitMasia), [matriax8c](https://twitter.com/DavitMasia/status/834862452164612096) palette.
* [Javier Guerrero](https://twitter.com/Xavier_Gd), [nyx8](https://twitter.com/Xavier_Gd/status/868519467864686594) palette.
* [Adigun A. Polack](https://twitter.com/adigunpolack), [AAP-64](http://pixeljoint.com/pixelart/119466.htm), [AAP-Splendor128](http://pixeljoint.com/pixelart/120714.htm), [SimpleJPC-16](http://pixeljoint.com/pixelart/119844.htm), and [AAP-Micro12](http://pixeljoint.com/pixelart/121151.htm) palette.
* [PineTreePizza](https://twitter.com/PineTreePizza), [Rosy-42](https://twitter.com/PineTreePizza/status/1006536191955623938) palette.
It tries to replicate some pixel-art algorithms:
* [RotSprite](http://forums.sonicretro.org/index.php?showtopic=8848&st=15&p=159754&#entry159754) by Xenowhirl.
* [Pixel perfect drawing algorithm](https://deepnight.net/blog/tools/pixel-perfect-drawing/) by [Sébastien Bénard](https://twitter.com/deepnightfr) and [Carduus](https://twitter.com/CarduusHimself/status/420554200737935361).
Thanks to [third-party open source projects](docs/LICENSES.md), to
[contributors](https://www.aseprite.org/contributors/), and all the
people who have contributed ideas, patches, bugs report, feature
requests, donations, and help us to develop Aseprite.
Check the [AUTHORS](AUTHORS.md) file for details about the active team
of developers working on Aseprite.
## License

133
build.sh
View File

@ -56,31 +56,6 @@ if [ "$1" == "--norun" ] ; then
norun=1
fi
# Platform.
if [[ "$(uname)" =~ "MINGW32" ]] || [[ "$(uname)" =~ "MINGW64" ]] || [[ "$(uname)" =~ "MSYS_NT-10.0" ]] ; then
is_win=1
cpu=x64
if ! cl.exe >/dev/null 2>/dev/null ; then
echo ""
echo "MSVC compiler (cl.exe) not found in PATH"
echo ""
echo " PATH=$PATH"
echo ""
exit 1
fi
elif [[ "$(uname)" == "Linux" ]] ; then
is_linux=1
cpu=x64
elif [[ "$(uname)" =~ "Darwin" ]] ; then
is_macos=1
if [[ $(uname -m) == "arm64" ]]; then
cpu=arm64
else
cpu=x64
fi
fi
# Check utilities.
if ! cmake --version >/dev/null ; then
echo ""
@ -137,6 +112,23 @@ if [ $run_submodule_update ] ; then
echo "Done"
fi
# Platform.
if ! source "$pwd/laf/misc/platform.sh" ; then
exit $?
fi
if [ $is_win ] ; then
# Check MSVC compiler.
if ! cl.exe >/dev/null 2>/dev/null ; then
echo ""
echo "MSVC compiler (cl.exe) not found in PATH"
echo ""
echo " PATH=$PATH"
echo ""
exit 1
fi
fi
# Create the directory to store the configuration.
if [ ! -d "$pwd/.build" ] ; then
mkdir "$pwd/.build"
@ -150,25 +142,25 @@ if [ ! -f "$pwd/.build/userkind" ] ; then
echo "user" > $pwd/.build/userkind
else
echo ""
echo "Select what kind of user you are (press U or D keys):"
echo "Select what kind of user you are (press U or D key and then Enter):"
echo ""
echo " [U]ser: give a try to Aseprite"
echo " [D]eveloper: develop/modify Aseprite"
echo ""
read -sN 1 -p "[U/D]? "
echo ""
if [[ "$REPLY" == "d" || "$REPLY" == "D" ]] ; then
read -p "[U/D]? "
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
if [[ "$REPLY" == "d" || "$REPLY" == "dev" || "$REPLY" == "developer" ]] ; then
echo "developer" > $pwd/.build/userkind
elif [[ "$REPLY" == "u" || "$REPLY" == "U" ]] ; then
elif [[ "$REPLY" == "u" || "$REPLY" == "user" ]] ; then
echo "user" > $pwd/.build/userkind
else
echo "Use U or D keys to select kind of user/build process"
echo "Use U or D keys (and press Enter) to select kind of user/build process"
exit 1
fi
fi
fi
userkind=$(echo -n $(cat $pwd/.build/userkind))
userkind=$(cat $pwd/.build/userkind)
if [ "$userkind" == "developer" ] ; then
echo "======================= BUILDING FOR DEVELOPER ======================="
else
@ -229,7 +221,7 @@ if [ ! -f "$pwd/.build/builds_dir" ] ; then
echo "$builds_dir" > "$pwd/.build/builds_dir"
fi
# Overwrite $builds_dir variable from the config content.
builds_dir="$(echo -n $(cat $pwd/.build/builds_dir))"
builds_dir="$(cat $pwd/.build/builds_dir)"
# List all builds.
builds_list="$(mktemp)"
@ -265,7 +257,8 @@ else
# New build
if [[ "$build_n" == "n" || "$build_n" == "N" ]] ; then
read -p "Select build type [RELEASE/debug]? "
if [[ "${REPLY,,}" == "debug" ]] ; then
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
if [[ "${REPLY}" == "debug" ]] ; then
build_type=Debug
new_build_name=aseprite-debug
else
@ -348,10 +341,7 @@ else
elif git --git-dir="$source_dir/.git" branch --contains "$remote/main" | grep -q "^\* $branch_name\$" ; then
base_branch_name=main
else
echo ""
echo "Error: Branch $branch_name looks like doesn't belong to main or beta"
echo ""
exit 1
base_branch_name=$branch_name
fi
fi
@ -366,15 +356,9 @@ else
fi
# Required Skia for the base branch.
if [ "$base_branch_name" == "beta" ] ; then
skia_tag=m124-08a5439a6b
file_skia_dir=beta_skia_dir
possible_skia_dir_name=skia-m124
else
skia_tag=m102-861e4743af
file_skia_dir=main_skia_dir
possible_skia_dir_name=skia
fi
skia_tag=$(cat "$pwd/laf/misc/skia-tag.txt")
possible_skia_dir_name=skia-$(echo $skia_tag | cut -d "-" -f 1)
file_skia_dir="$base_branch_name"_skia_dir
# Check Skia dependency.
if [ ! -f "$pwd/.build/$file_skia_dir" ] ; then
@ -385,23 +369,33 @@ if [ ! -f "$pwd/.build/$file_skia_dir" ] ; then
skia_dir="$HOME/deps/$possible_skia_dir_name"
fi
# Set default location if not found
if [ ! -d "$skia_dir" ] ; then
echo ""
echo "Skia directory wasn't found."
echo ""
echo "Select Skia directory to create [$skia_dir]? "
if [ ! $auto ] ; then
read skia_dir_read
if [ "$skia_dir_read" != "" ] ; then
skia_dir="$skia_dir_read"
fi
# Use .deps directory to download Skia for users (which is a
# simple setup). In case of developers we'd prefer the shared
# directory by default.
if [ "$userkind" == "user" ] ; then
skia_dir="$pwd/.deps/$possible_skia_dir_name"
fi
if [ ! -d "$skia_dir" ] ; then
echo ""
echo "Skia directory wasn't found."
echo ""
echo "Select Skia directory to create [$skia_dir]? "
if [ ! $auto ] ; then
read skia_dir_read
if [ "$skia_dir_read" != "" ] ; then
skia_dir="$skia_dir_read"
fi
fi
mkdir -p $skia_dir || exit 1
fi
mkdir -p $skia_dir || exit 1
fi
echo $skia_dir > "$pwd/.build/$file_skia_dir"
fi
skia_dir=$(echo -n $(cat $pwd/.build/$file_skia_dir))
skia_dir=$(cat $pwd/.build/$file_skia_dir)
if [ ! -d "$skia_dir" ] ; then
mkdir "$skia_dir"
fi
@ -421,25 +415,20 @@ if [ ! -d "$skia_library_dir" ] ; then
echo "Skia library wasn't found."
echo ""
if [ ! $auto ] ; then
read -sN 1 -p "Download pre-compiled Skia automatically [Y/n]? "
read -p "Download pre-compiled Skia automatically [Y/n]? "
# Convert the Enter key as the default option: an empty string
REPLY=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
fi
if [[ $auto || "$REPLY" == "" || "$REPLY" == "y" || "$REPLY" == "Y" ]] ; then
if [[ $auto || "$REPLY" == "" || "$REPLY" == "y" || "$REPLY" == "yes" ]] ; then
if [[ $is_win && "$build_type" == "Debug" ]] ; then
skia_build=Debug
else
skia_build=Release
fi
if [ $is_win ] ; then
skia_file=Skia-Windows-$skia_build-$cpu.zip
elif [ $is_macos ] ; then
skia_file=Skia-macOS-$skia_build-$cpu.zip
else
skia_file=Skia-Linux-$skia_build-$cpu-libstdc++.zip
fi
skia_url=https://github.com/aseprite/skia/releases/download/$skia_tag/$skia_file
skia_url=$(bash laf/misc/skia-url.sh $skia_build)
skia_file=$(basename $skia_url)
if [ ! -f "$skia_dir/$skia_file" ] ; then
curl -L -o "$skia_dir/$skia_file" "$skia_url"
curl --ssl-revoke-best-effort -L -o "$skia_dir/$skia_file" "$skia_url"
fi
if [ ! -d "$skia_library_dir" ] ; then
unzip -n -d "$skia_dir" "$skia_dir/$skia_file"
@ -468,7 +457,7 @@ if [ ! -f "$active_build_dir/ninja.build" ] ; then
echo "This will take some minutes."
echo ""
if [ ! $auto ] ; then
read -sN 1 -p "Press any key to continue. "
read -p "Press Enter to continue."
fi
if [ $is_macos ] ; then

View File

@ -709,10 +709,11 @@
<style id="workspace_splitter">
<background color="workspace" />
</style>
<style id="horizontal_separator" border-left="2" border-top="4" border-right="2" border-bottom="0">
<style id="horizontal_separator" border="2">
<background color="window_face" />
<background-border part="separator_horz" align="middle" />
<text color="separator_label" x="4" align="left middle" />
<text color="disabled" x="4" align="left middle" state="disabled"/>
</style>
<style id="menu_separator" extends="horizontal_separator" />
<style id="separator_in_view" extends="horizontal_separator">

View File

@ -702,10 +702,11 @@
<style id="workspace_splitter">
<background color="workspace" />
</style>
<style id="horizontal_separator" border-left="2" border-top="4" border-right="2" border-bottom="0">
<style id="horizontal_separator" border="2">
<background color="window_face" />
<background-border part="separator_horz" align="middle" />
<text color="separator_label" x="4" align="left middle" />
<text color="disabled" x="4" align="left middle" state="disabled"/>
</style>
<style id="menu_separator" extends="horizontal_separator" />
<style id="separator_in_view" extends="horizontal_separator">
@ -1042,6 +1043,12 @@
<text color="slider_empty_text" align="center middle" />
<text color="slider_empty_text" align="center middle" state="focus" y="1" />
</style>
<style id="textedit" border="2">
<background color="textbox_face" />
<text color="textbox_text" align="left" />
<text color="selected" align="left" state="selected" />
<text color="selected_text" align="left" state="selected" />
</style>
<style id="mini_slider" extends="slider" font="mini">
<background part="mini_slider_empty" />
<text color="slider_empty_text" align="center middle" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -34,14 +34,14 @@
type="spritesheet"
descent="2"
file="aseprite_font.png">
<fallback font="Unicode" size="8" />
<fallback font="Unicode" size="14" />
</font>
<font name="Aseprite Mini"
type="spritesheet"
descent="1"
file="aseprite_mini.png">
<fallback font="Unicode" size="6" />
<fallback font="Unicode" size="10" />
</font>
</fonts>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
<!-- Copyright (C) 2014-2018 David Capello -->
<preferences>
@ -356,6 +356,7 @@
<section id="file_selector">
<option id="current_folder" type="std::string" default="&quot;&lt;empty&gt;&quot;" />
<option id="zoom" type="double" default="1.0" />
<option id="show_hidden" type="bool" default="false" />
</section>
<section id="text_tool">
<option id="font_face" type="std::string" />
@ -550,6 +551,7 @@
<option id="enabled" type="bool" default="false" />
<option id="overlay_enabled" type="bool" default="false" />
<option id="overlay_size" type="int" default="5" />
<option id="scale_up_to_fit" type="bool" default="false" />
</section>
<section id="onionskin">
<option id="active" type="bool" default="false" />

View File

@ -765,6 +765,7 @@ pinned_folders = Pinned Folders
recent_folders = Recent Folders
all_formats = All formats
all_files = All files
show_hidden = Show hidden
[filters]
selected_cels = Selected
@ -785,6 +786,11 @@ load = Load External Font
select_truetype_fonts = Select a Font File
empty_fonts = No system fonts were found
[font_style]
antialias = Antialias
hinting = Hinting
ligatures = Ligatures
[frame_combo]
all_frames = All frames
selected_frames = Selected frames
@ -1666,6 +1672,11 @@ n_slices_removed = {} slice(s) removed
x_removed = Layer "{}" removed
layers_removed = Layers removed
[resource_listbox]
loading = Loading
pin = Pin this item
unpin = Unpin this item
[save_file]
title = Save File
save_as = Save As
@ -1682,7 +1693,8 @@ title = Save Selection (.msk file)
[script_access]
title = Security
script_label = The following script:
file_label = wants to access to this file:
file_label = wants to access this file:
file_write_label = wants to write to this file:
command_label = wants to execute the following command:
websocket_label = wants to open a WebSocket connection to this URL:
clipboard_label = wants to access the system clipboard
@ -1696,7 +1708,7 @@ allow_load_lib_access = &Allow Load External Library
give_full_access = Give Script Full &Access
stop_script = &Stop Script
[select_accelerator]
[select_shortcut]
title = Keyboard Shortcut
key = Key:
clear = Clear
@ -1823,6 +1835,18 @@ pixel_scale = Pixel Scale
with_vars = Use CSS3 Variables
generate_html = Generate Sample HTML File
[shape]
fill = Fill
stroke = Stroke
stroke_width = Stroke Width
[text_tool]
font_family = Font Family
font_size = Font Size
bold = Bold
italic = Italic
more_options = More Options
[timeline_conf]
position = Position:
left = &Left
@ -1833,6 +1857,7 @@ first_frame = First Frame:
thumbnails = Thumbnails
thumbnail_size = Thumbnail Size:
overlay_size = Overlay Size:
scale_up_to_fit = Scale up to fit
onion_skin = Onion Skin:
merge_frames = Merge Frames
red_blue_tint = Red/Blue Tint
@ -1845,6 +1870,7 @@ behind_sprite = Behind sprite
behind_sprite_toolip = Only for transparent layers.\nBackground is not included in this onion skin mode.
in_front = In front of sprite
in_front_toolip = For all kind of layers (background and transparent)
set_as_defaults = Set as Defaults
[tools]
rectangular_marquee = Rectangular Marquee Tool
@ -1883,6 +1909,7 @@ timeline_show = Show Timeline
[undo_history]
title = Undo History
initial_state = Initial State
[user_data]
user_data = User Data:

View File

@ -1,29 +1,14 @@
<!-- Aseprite -->
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
<!-- Copyright (C) 2018 David Capello -->
<gui i18nwarnings="false">
<window id="about" text="About Aseprite">
<vbox>
<label text="" id="title" />
<label text="Animated sprite editor &amp;&amp; pixel art tool" />
<hbox homogeneous="true">
<hbox>
<vbox expansive="true">
<separator text="Developer Team" horizontal="true" />
<link text="David Capello" url="https://twitter.com/davidcapello" />
<link text="Gaspar Capello" url="https://twitter.com/Gasparoken" />
<link text="Martín Capello" url="https://twitter.com/martincapell0" />
<vbox minheight="8" />
</vbox>
<separator vertical="true" />
</hbox>
<vbox>
<separator text="Credits" horizontal="true" cell_hspan="2" />
<link text="Contributors" url="" id="credits" />
<link text="Translators" url="" id="i18n_credits" />
<link text="Open Source Projects" url="" id="licenses" />
</vbox>
</hbox>
<link text="Authors &amp;&amp; Credits" url="" id="credits" />
<link text="Translators" url="" id="i18n_credits" />
<link text="Open Source Projects" url="" id="licenses" />
<separator horizontal="true" />
<hbox>
<label text="Copyright (C) 2001-2025" />

View File

@ -23,6 +23,7 @@
<combobox id="location" expansive="true" />
<button text="" id="refresh_button" style="refresh_button"
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
<check id="show_hidden_check" text="@.show_hidden" />
</box>
<vbox id="file_view_placeholder" expansive="true" />
<grid columns="2">

View File

@ -0,0 +1,10 @@
<!-- Aseprite -->
<!-- Copyright (C) 2025 by Igara Studio S.A. -->
<gui>
<vbox id="font_style">
<check id="antialias" text="@.antialias" />
<check id="hinting" text="@.hinting" />
<separator horizontal="true" />
<check id="ligatures" text="@.ligatures" />
</vbox>
</gui>

View File

@ -4,7 +4,7 @@
<gui>
<window id="layer_properties" text="@.title">
<vbox>
<grid id="properties_grid" columns="3">
<grid id="properties_grid" columns="3" expansive="true">
<label text="@.name" />
<entry text="" id="name" magnet="true" maxsize="256" minwidth="64" cell_align="horizontal" />
<button id="user_data" icon="icon_user_data" tooltip="@general.user_data" />

View File

@ -1,7 +1,8 @@
<!-- Aseprite -->
<!-- Copyright (C) 2025 by Igara Studio S.A. -->
<!-- Copyright (C) 2001-2016 by David Capello -->
<gui>
<window id="select_accelerator" text="@.title">
<window id="select_shortcut" text="@.title">
<vbox expansive="true">
<grid columns="3">
<label text="@.key" />

View File

@ -1,4 +1,5 @@
<!-- Aseprite -->
<!-- Copyright (C) 2025 Igara Studio S.A. -->
<!-- Copyright (C) 2014-2018 by David Capello -->
<gui>
<vbox id="timeline_conf">
@ -30,6 +31,7 @@
<check id="thumb_overlay_enabled" text="@.overlay_size"/>
<slider min="2" max="10" id="thumb_overlay_size" cell_align="horizontal" width="128" />
<check id="thumb_scale_up_to_fit" text="@.scale_up_to_fit" cell_hspan="2" />
</grid>
</vbox>
</hbox>
@ -55,5 +57,11 @@
<radio group="2" text="@.in_front" id="infront" tooltip="@.in_front_toolip" />
</hbox>
</grid>
<separator horizontal="true" />
<hbox>
<boxfiller />
<button id="defaults" text="@.set_as_defaults" />
</hbox>
</vbox>
</gui>

View File

@ -6,6 +6,8 @@
<label id="color_label" text="@.color" />
<label id="entry_label" text="@.user_data" />
<colorpicker id="color" simple="true" expansive="true" />
<entry id="entry" maxsize="65535" minwidth="128" expansive="true" />
<view id="text_edit_view" height="30" expansive="true">
<textedit id="text_edit" />
</view>
</hbox>
</gui>

View File

@ -1,5 +1,34 @@
Aseprite uses the following open source projects:
* [Allegro 4](http://liballeg.org/)
* [Bresenham algorithm implementations by Alois Zingl](http://members.chello.at/easyfilter/bresenham.html)
* [cityhash](https://github.com/google/cityhash)
* [cmark](https://github.com/jgm/cmark)
* [curl](http://curl.haxx.se/)
* [fmt](https://github.com/fmtlib/fmt)
* [FreeType](http://www.freetype.org/)
* [giflib](http://sourceforge.net/projects/giflib/)
* [Google Test](https://github.com/google/googletest)
* [harfbuzz](http://harfbuzz.org)
* [IXWebSocket](https://github.com/machinezone/IXWebSocket)
* [json11](https://github.com/dropbox/json11/)
* [libarchive](http://www.libarchive.org/)
* [libjpeg-turbo](https://libjpeg-turbo.org/)
* [libpng](http://www.libpng.org/pub/png/)
* [libwebp](https://developers.google.com/speed/webp/)
* [Lua](https://www.lua.org/)
* [pixman](http://www.pixman.org/)
* [qoi](https://github.com/phoboslab/qoi)
* [Sentry](https://sentry.io)
* [skia](https://skia.org)
* [simpleini](https://github.com/aseprite/simpleini/)
* [TinyEXIF](https://github.com/cdcseacave/TinyEXIF)
* [tinyexpr](https://github.com/codeplea/tinyexpr)
* [tinyxml2](https://github.com/leethomason/tinyxml2)
* [ucdn](https://github.com/grigorig/ucdn)
* [Wintab API](http://www.wacomeng.com/windows/docs/WintabBackground.htm)
* [zlib](http://www.zlib.net/)
# [Allegro 4](http://liballeg.org/)
```

2
laf

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

View File

@ -1,5 +1,5 @@
# Aseprite
# Copyright (C) 2019-2024 Igara Studio S.A.
# Copyright (C) 2019-2025 Igara Studio S.A.
# Copyright (C) 2001-2018 David Capello
######################################################################
@ -152,6 +152,12 @@ add_custom_command(
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../README.md)
list(APPEND out_data_files ${DATA_OUTPUT_DIR}/README.md)
add_custom_command(
OUTPUT ${DATA_OUTPUT_DIR}/AUTHORS.md
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../AUTHORS.md ${DATA_OUTPUT_DIR}/AUTHORS.md
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../AUTHORS.md)
list(APPEND out_data_files ${DATA_OUTPUT_DIR}/AUTHORS.md)
add_custom_command(
OUTPUT ${DATA_OUTPUT_DIR}/EULA.txt
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../EULA.txt ${DATA_OUTPUT_DIR}/EULA.txt
@ -174,8 +180,8 @@ if(ENABLE_ASEPRITE_EXE)
if(WIN32)
set(main_resources
main/resources_win32.rc
main/settings.manifest)
main/win/resources_win32.rc
main/win/settings.manifest)
endif()
add_executable(${main_target}

View File

@ -72,6 +72,10 @@ In Debug mode (`_DEBUG`):
* [`TRACEARGS`](https://github.com/aseprite/laf/blob/f3222bdee2d21556e9da55343e73803c730ecd97/base/debug.h#L40):
in debug mode, it prints in the terminal/console each given argument
In release mode you can use a similar function:
* `PRINTARGS` prints in the terminal/console each given argument
# Detect Platform
You can check the platform using some `laf` macros:

View File

@ -108,7 +108,7 @@ endif()
# This defines a specific webp decoding utility function for using
# in Windows when dragging and dropping images that are stored as
# webp files (like Chrome does).
if(WIN32 AND ENABLE_WEBP)
if(WIN32 AND ENABLE_WEBP AND LAF_BACKEND STREQUAL "skia")
target_sources(app-lib PRIVATE util/decode_webp.cpp)
endif()
@ -290,7 +290,6 @@ target_sources(app-lib PRIVATE
cmd/copy_region.cpp
cmd/crop_cel.cpp
cmd/deselect_mask.cpp
cmd/drop_on_timeline.cpp
cmd/flatten_layers.cpp
cmd/flip_image.cpp
cmd/flip_mask.cpp
@ -526,6 +525,7 @@ target_sources(app-lib PRIVATE
context_flags.cpp
doc.cpp
doc_api.cpp
doc_api_dnd_helper.cpp
doc_diff.cpp
doc_exporter.cpp
doc_range_ops.cpp
@ -669,7 +669,7 @@ target_sources(app-lib PRIVATE
ui/rgbmap_algorithm_selector.cpp
ui/sampling_selector.cpp
ui/search_entry.cpp
ui/select_accelerator.cpp
ui/select_shortcut.cpp
ui/selection_mode_field.cpp
ui/skin/skin_part.cpp
ui/skin/skin_property.cpp
@ -684,6 +684,7 @@ target_sources(app-lib PRIVATE
ui/tile_button.cpp
ui/tileset_selector.cpp
ui/timeline/ani_controls.cpp
ui/timeline/doc_providers.cpp
ui/timeline/timeline.cpp
ui/toolbar.cpp
ui/user_data_view.cpp
@ -706,6 +707,7 @@ target_sources(app-lib PRIVATE
util/layer_utils.cpp
util/msk_file.cpp
util/new_image_from_mask.cpp
util/open_file_job.cpp
util/pal_ops.cpp
util/pic_file.cpp
util/pixel_ratio.cpp

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -79,7 +79,7 @@
#include "os/x11/system.h"
#endif
#if ENABLE_WEBP && LAF_WINDOWS
#if ENABLE_WEBP && LAF_WINDOWS && LAF_SKIA
#include "app/util/decode_webp.h"
#endif
@ -485,7 +485,7 @@ void App::run(const bool runGuiManager)
// How to interpret one finger on Windows tablets.
manager->display()->nativeWindow()->setInterpretOneFingerGestureAsMouseMovement(
preferences().experimental.oneFingerAsMouseMovement());
#if ENABLE_WEBP
#if ENABLE_WEBP && LAF_SKIA
// In Windows we use a custom webp decoder for drag & drop operations.
os::set_decode_webp(util::decode_webp);
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -113,11 +113,11 @@ bool can_call_global_shortcut(const AppMenuItem::Native* native)
(focus == nullptr || focus->type() != ui::kEntryWidget ||
!is_text_entry_shortcut(native->shortcut)) &&
(native->keyContext == KeyContext::Any ||
native->keyContext == KeyboardShortcuts::instance()->getCurrentKeyContext());
native->keyContext == KeyboardShortcuts::getCurrentKeyContext());
}
// TODO this should be on "she" library (or we should use
// os::Shortcut instead of ui::Accelerators)
// TODO this should be on laf-os library (or we should use
// os::Shortcut instead of ui::Shortcuts)
int from_scancode_to_unicode(KeyScancode scancode)
{
static int map[] = {
@ -284,22 +284,21 @@ void destroy_menu_item(ui::Widget* item)
os::Shortcut get_os_shortcut_from_key(const Key* key)
{
if (key && !key->accels().empty()) {
const ui::Accelerator& accel = key->accels().front();
if (key && !key->shortcuts().empty()) {
const ui::Shortcut& shortcut = key->shortcuts().front();
#if LAF_MACOS
// Shortcuts with spacebar as modifier do not work well in macOS
// (they will be called when the space bar is unpressed too).
if ((accel.modifiers() & ui::kKeySpaceModifier) == ui::kKeySpaceModifier)
if ((shortcut.modifiers() & ui::kKeySpaceModifier) == ui::kKeySpaceModifier)
return os::Shortcut();
#endif
return os::Shortcut(
(accel.unicodeChar() ? accel.unicodeChar() : from_scancode_to_unicode(accel.scancode())),
accel.modifiers());
return os::Shortcut((shortcut.unicodeChar() ? shortcut.unicodeChar() :
from_scancode_to_unicode(shortcut.scancode())),
shortcut.modifiers());
}
else
return os::Shortcut();
return {};
}
AppMenus* AppMenus::s_instance = nullptr;
@ -719,7 +718,6 @@ Widget* AppMenus::convertXmlelemToMenuitem(XMLElement* elem, Menu* menu)
{
const char* id = elem->Attribute("id");
const char* group = elem->Attribute("group");
const char* standard = elem->Attribute("standard");
// is it a <separator>?
if (strcmp(elem->Value(), "separator") == 0) {
@ -781,6 +779,7 @@ Widget* AppMenus::convertXmlelemToMenuitem(XMLElement* elem, Menu* menu)
}
#if LAF_MACOS
const char* standard = elem->Attribute("standard");
if (standard && strcmp(standard, "edit") == 0)
menuitem->setAsStandardEditMenu();
#endif

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -20,7 +21,20 @@ namespace app { namespace cmd {
using namespace doc;
ClearRect::ClearRect(Cel* cel, const gfx::Rect& bounds, color_t color)
{
ASSERT(cel);
initialize(cel, bounds, color);
}
ClearRect::ClearRect(Cel* cel, const gfx::Rect& bounds)
{
ASSERT(cel);
Doc* doc = static_cast<Doc*>(cel->document());
initialize(cel, bounds, doc->bgColor(cel->layer()));
}
void ClearRect::initialize(Cel* cel, const gfx::Rect& bounds, color_t color)
{
ASSERT(cel);
@ -37,9 +51,7 @@ ClearRect::ClearRect(Cel* cel, const gfx::Rect& bounds)
return;
m_dstImage.reset(new WithImage(image));
Doc* doc = static_cast<Doc*>(cel->document());
m_bgcolor = doc->bgColor(cel->layer());
m_bgcolor = color;
m_copy.reset(crop_image(image, bounds2.x, bounds2.y, bounds2.w, bounds2.h, m_bgcolor));
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -26,6 +27,7 @@ using namespace doc;
class ClearRect : public Cmd {
public:
ClearRect(Cel* cel, const gfx::Rect& bounds);
ClearRect(Cel* cel, const gfx::Rect& bounds, color_t color);
protected:
void onExecute() override;
@ -37,6 +39,7 @@ protected:
}
private:
void initialize(Cel* cel, const gfx::Rect& bounds, color_t color);
void clear();
void restore();

View File

@ -1,397 +0,0 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/cmd/drop_on_timeline.h"
#include "app/cmd/add_layer.h"
#include "app/cmd/move_cel.h"
#include "app/cmd/set_pixel_format.h"
#include "app/console.h"
#include "app/context_flags.h"
#include "app/doc.h"
#include "app/doc_event.h"
#include "app/file/file.h"
#include "app/tx.h"
#include "app/util/layer_utils.h"
#include "app/util/open_file_job.h"
#include "base/serialization.h"
#include "doc/layer_io.h"
#include "doc/layer_list.h"
#include "doc/subobjects_io.h"
#include "render/dithering.h"
#include <algorithm>
namespace app { namespace cmd {
using namespace base::serialization::little_endian;
DropOnTimeline::DropOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
InsertionPoint insert,
DroppedOn droppedOn,
const base::paths& paths)
: WithDocument(doc)
, m_size(0)
, m_paths(paths)
, m_frame(frame)
, m_layerIndex(layerIndex)
, m_insert(insert)
, m_droppedOn(droppedOn)
{
ASSERT(m_layerIndex >= 0);
for (const auto& path : m_paths)
m_size += path.size();
// Zero layers stored.
write32(m_stream, 0);
m_size += sizeof(uint32_t);
}
DropOnTimeline::DropOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
InsertionPoint insert,
DroppedOn droppedOn,
const doc::ImageRef& image)
: WithDocument(doc)
, m_size(0)
, m_image(image)
, m_frame(frame)
, m_layerIndex(layerIndex)
, m_insert(insert)
, m_droppedOn(droppedOn)
{
ASSERT(m_layerIndex >= 0);
// Zero layers stored.
write32(m_stream, 0);
m_size += sizeof(uint32_t);
}
void DropOnTimeline::onExecute()
{
Doc* destDoc = document();
m_previousTotalFrames = destDoc->sprite()->totalFrames();
int docsProcessed = 0;
while (hasPendingWork()) {
std::unique_ptr<Doc> srcDoc;
if (!getNextDoc(srcDoc))
return;
if (srcDoc) {
docsProcessed++;
// If source document doesn't match the destination document's color
// mode, change it.
if (srcDoc->colorMode() != destDoc->colorMode()) {
// Execute in a source doc transaction because we don't need undo/redo
// this.
Tx tx(srcDoc.get());
tx(new cmd::SetPixelFormat(srcDoc->sprite(),
destDoc->sprite()->pixelFormat(),
render::Dithering(),
Preferences::instance().quantization.rgbmapAlgorithm(),
nullptr,
nullptr,
FitCriteria::DEFAULT));
tx.commit();
}
// If there is only one source document to process and it has a cel that
// can be moved, then move the cel from the source doc into the
// destination doc's selected frame.
const bool isJustOneDoc = (docsProcessed == 1 && !hasPendingWork());
if (isJustOneDoc && canMoveCelFrom(srcDoc.get())) {
auto* srcLayer = static_cast<LayerImage*>(srcDoc->sprite()->firstLayer());
auto* destLayer = static_cast<LayerImage*>(destDoc->sprite()->allLayers()[m_layerIndex]);
executeAndAdd(new MoveCel(srcLayer, 0, destLayer, m_frame, false));
break;
}
// If there is no room for the source frames, add frames to the
// destination sprite.
if (m_frame + srcDoc->sprite()->totalFrames() > destDoc->sprite()->totalFrames()) {
destDoc->sprite()->setTotalFrames(m_frame + srcDoc->sprite()->totalFrames());
}
// Save dropped layers from source document.
saveDroppedLayers(srcDoc->sprite()->root()->layers(), destDoc->sprite());
}
}
if (m_droppedLayersIds.empty())
return;
destDoc->sprite()->incrementVersion();
destDoc->incrementVersion();
insertDroppedLayers();
}
void DropOnTimeline::onUndo()
{
CmdSequence::onUndo();
if (m_droppedLayersIds.empty()) {
notifyGeneralUpdate();
return;
}
Doc* doc = document();
frame_t currentTotalFrames = doc->sprite()->totalFrames();
for (auto id : m_droppedLayersIds) {
auto* layer = doc::get<Layer>(id);
ASSERT(layer);
if (layer) {
DocEvent ev(doc);
ev.sprite(layer->sprite());
ev.layer(layer);
doc->notify_observers<DocEvent&>(&DocObserver::onBeforeRemoveLayer, ev);
LayerGroup* group = layer->parent();
group->removeLayer(layer);
group->incrementVersion();
group->sprite()->incrementVersion();
doc->notify_observers<DocEvent&>(&DocObserver::onAfterRemoveLayer, ev);
delete layer;
}
}
doc->sprite()->setTotalFrames(m_previousTotalFrames);
doc->sprite()->incrementVersion();
m_previousTotalFrames = currentTotalFrames;
}
void DropOnTimeline::onRedo()
{
CmdSequence::onRedo();
if (m_droppedLayersIds.empty()) {
notifyGeneralUpdate();
return;
}
Doc* doc = document();
frame_t currentTotalFrames = doc->sprite()->totalFrames();
doc->sprite()->setTotalFrames(m_previousTotalFrames);
doc->sprite()->incrementVersion();
m_previousTotalFrames = currentTotalFrames;
insertDroppedLayers();
}
void DropOnTimeline::setupInsertionLayer(Layer*& layer, LayerGroup*& group)
{
const LayerList& allLayers = document()->sprite()->allLayers();
layer = allLayers[m_layerIndex];
if (m_insert == InsertionPoint::BeforeLayer && layer->isGroup()) {
group = static_cast<LayerGroup*>(layer);
// The user is trying to drop layers into an empty group, so there is no after
// nor before layer...
if (group->layersCount() == 0) {
layer = nullptr;
return;
}
layer = group->lastLayer();
m_insert = InsertionPoint::AfterLayer;
}
group = layer->parent();
}
bool DropOnTimeline::hasPendingWork()
{
return m_image || !m_paths.empty();
}
bool DropOnTimeline::getNextDocFromImage(std::unique_ptr<Doc>& srcDoc)
{
if (!m_image)
return true;
Sprite* sprite = new Sprite(m_image->spec(), 256);
LayerImage* layer = new LayerImage(sprite);
sprite->root()->addLayer(layer);
Cel* cel = new Cel(0, m_image);
layer->addCel(cel);
srcDoc = std::make_unique<Doc>(sprite);
m_image = nullptr;
return true;
}
bool DropOnTimeline::getNextDocFromPaths(std::unique_ptr<Doc>& srcDoc)
{
Console console;
Context* context = document()->context();
int flags = FILE_LOAD_DATA_FILE | FILE_LOAD_AVOID_BACKGROUND_LAYER | FILE_LOAD_CREATE_PALETTE |
FILE_LOAD_SEQUENCE_YES;
std::unique_ptr<FileOp> fop(FileOp::createLoadDocumentOperation(context, m_paths.front(), flags));
// Remove the path that is currently being processed
m_paths.erase(m_paths.begin());
// Do nothing (the user cancelled or something like that)
if (!fop)
return false;
if (fop->hasError()) {
console.printf(fop->error().c_str());
return true;
}
base::paths fopFilenames;
fop->getFilenameList(fopFilenames);
// Remove paths that will be loaded by the current file operation.
for (const auto& filename : fopFilenames) {
auto it = std::find(m_paths.begin(), m_paths.end(), filename);
if (it != m_paths.end())
m_paths.erase(it);
}
OpenFileJob task(fop.get(), true);
task.showProgressWindow();
// Post-load processing, it is called from the GUI because may require user intervention.
fop->postLoad();
// Show any error
if (fop->hasError() && !fop->isStop())
console.printf(fop->error().c_str());
srcDoc.reset(fop->releaseDocument());
return true;
}
bool DropOnTimeline::getNextDoc(std::unique_ptr<Doc>& srcDoc)
{
if (m_image == nullptr && !m_paths.empty())
return getNextDocFromPaths(srcDoc);
return getNextDocFromImage(srcDoc);
}
void DropOnTimeline::storeDroppedLayerIds(const Layer* layer)
{
if (layer->isGroup()) {
const auto* group = static_cast<const LayerGroup*>(layer);
for (auto* child : group->layers())
storeDroppedLayerIds(child);
m_droppedLayersIds.push_back(group->id());
}
else {
m_droppedLayersIds.push_back(layer->id());
}
}
void DropOnTimeline::saveDroppedLayers(const LayerList& layers, Sprite* sprite)
{
size_t start = m_stream.tellp();
// Calculate the new number of layers.
m_stream.seekg(0);
uint32_t nLayers = read32(m_stream) + layers.size();
// Flat list of all the dropped layers.
LayerList allDroppedLayers;
// Write number of layers (at the beginning of the stream).
m_stream.seekp(0);
write32(m_stream, nLayers);
// Move to where we must start writing.
m_stream.seekp(start);
for (auto it = layers.cbegin(); it != layers.cend(); ++it) {
auto* layer = *it;
// TODO: If we could "relocate" a layer from the source document to the
// destination document we could avoid making a copy here.
std::unique_ptr<Layer> layerCopy(copy_layer_with_sprite(layer, sprite));
layerCopy->displaceFrames(0, m_frame);
write_layer(m_stream, layerCopy.get());
storeDroppedLayerIds(layerCopy.get());
}
size_t end = m_stream.tellp();
m_size += end - start;
}
void DropOnTimeline::insertDroppedLayers()
{
// Layer used as a reference to determine if the dropped layers will be
// inserted after or before it.
Layer* refLayer = nullptr;
// Parent group of the reference layer layer.
LayerGroup* group = nullptr;
// Keep track of the current insertion point.
InsertionPoint insert = m_insert;
setupInsertionLayer(refLayer, group);
SubObjectsFromSprite io(group->sprite());
m_stream.seekg(0);
auto nLayers = read32(m_stream);
for (int i = 0; i < nLayers; ++i) {
auto* layer = read_layer(m_stream, &io);
if (!refLayer) {
group->addLayer(layer);
refLayer = layer;
insert = InsertionPoint::AfterLayer;
}
else if (insert == InsertionPoint::AfterLayer) {
group->insertLayer(layer, refLayer);
refLayer = layer;
}
else if (insert == InsertionPoint::BeforeLayer) {
group->insertLayerBefore(layer, refLayer);
refLayer = layer;
insert = InsertionPoint::AfterLayer;
}
group->incrementVersion();
group->sprite()->incrementVersion();
Doc* doc = static_cast<Doc*>(group->sprite()->document());
DocEvent ev(doc);
ev.sprite(group->sprite());
ev.layer(layer);
doc->notify_observers<DocEvent&>(&DocObserver::onAddLayer, ev);
}
}
// Returns true if the document srcDoc has a cel that can be moved.
// The cel from the srcDoc can be moved only when all of the following
// conditions are met:
// * Drop took place in a cel.
// * Source doc has only one layer with just one frame.
// * The layer from the source doc and the destination cel's layer are both
// Image layers.
// Otherwise this function returns false.
bool DropOnTimeline::canMoveCelFrom(app::Doc* srcDoc)
{
auto* srcLayer = srcDoc->sprite()->firstLayer();
auto* destLayer = document()->sprite()->allLayers()[m_layerIndex];
return m_droppedOn == DroppedOn::Cel && srcDoc->sprite()->allLayersCount() == 1 &&
srcDoc->sprite()->totalFrames() == 1 && srcLayer->isImage() && destLayer->isImage();
}
void DropOnTimeline::notifyGeneralUpdate()
{
Doc* doc = document();
if (!doc)
return;
doc->notifyGeneralUpdate();
}
}} // namespace app::cmd

View File

@ -1,95 +0,0 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_CMD_drop_on_timeline_H_INCLUDED
#define APP_CMD_drop_on_timeline_H_INCLUDED
#pragma once
#include "app/cmd/with_document.h"
#include "app/cmd_sequence.h"
#include "app/doc_observer.h"
#include "base/paths.h"
#include "doc/frame.h"
#include "doc/image_ref.h"
#include "doc/layer.h"
#include "doc/layer_list.h"
namespace app { namespace cmd {
class DropOnTimeline : public CmdSequence,
public WithDocument {
public:
enum class InsertionPoint {
BeforeLayer,
AfterLayer,
};
enum class DroppedOn {
Unspecified,
Frame,
Layer,
Cel,
};
// Inserts the layers and frames of the documents pointed by the specified
// paths, at the specified frame and before or after the specified layer index.
DropOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
InsertionPoint insert,
DroppedOn droppedOn,
const base::paths& paths);
// Inserts the image as if it were a document with just one layer and one
// frame, at the specified frame and before or after the specified layer index.
DropOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
InsertionPoint insert,
DroppedOn droppedOn,
const doc::ImageRef& image);
protected:
void onExecute() override;
void onUndo() override;
void onRedo() override;
size_t onMemSize() const override { return sizeof(*this) + m_size; }
private:
void setupInsertionLayer(doc::Layer*& layer, doc::LayerGroup*& group);
void insertDroppedLayers();
bool canMoveCelFrom(app::Doc* srcDoc);
void notifyGeneralUpdate();
bool hasPendingWork();
// Returns the next document to be processed.
// Returns false when the user cancelled the process, or true when the
// process must go on.
bool getNextDoc(std::unique_ptr<Doc>& srcDoc);
bool getNextDocFromImage(std::unique_ptr<Doc>& srcDoc);
bool getNextDocFromPaths(std::unique_ptr<Doc>& srcDoc);
void storeDroppedLayerIds(const doc::Layer* layer);
void saveDroppedLayers(const doc::LayerList& layers, doc::Sprite* sprite);
size_t m_size;
base::paths m_paths;
doc::ImageRef m_image = nullptr;
doc::frame_t m_frame;
doc::layer_t m_layerIndex;
InsertionPoint m_insert;
DroppedOn m_droppedOn;
// Serialized dropped layers' data. Used for redo operation.
std::stringstream m_stream;
// Holds the Object IDs of the dropped layers. Used when determining which
// layers should be removed in an undo operation.
std::vector<doc::ObjectId> m_droppedLayersIds;
// Number of frames the doc had before dropping.
doc::frame_t m_previousTotalFrames;
};
}} // namespace app::cmd
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2024 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -44,7 +44,7 @@ void AboutCommand::onExecute(Context* context)
});
window.credits()->Click.connect([&window] {
window.closeWindow(nullptr);
App::instance()->mainWindow()->showBrowser("README.md", "Authors");
App::instance()->mainWindow()->showBrowser("AUTHORS.md", "Authors");
});
window.i18nCredits()->Click.connect([&window] {
window.closeWindow(nullptr);

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -54,11 +55,11 @@ void AdvancedModeCommand::onExecute(Context* context)
if (oldMode == MainWindow::NormalMode && pref.advancedMode.showAlert()) {
KeyPtr key = KeyboardShortcuts::instance()->command(this->id().c_str());
if (!key->accels().empty()) {
if (!key->shortcuts().empty()) {
app::gen::AdvancedMode window;
window.warningLabel()->setTextf("You can go back pressing \"%s\" key.",
key->accels().front().toString().c_str());
key->shortcuts().front().toString().c_str());
window.openWindowInForeground();

View File

@ -345,7 +345,7 @@ private:
color_t c = m_cel->data()->userData().color();
m_userDataView.color()->setColor(
Color::fromRgb(rgba_getr(c), rgba_getg(c), rgba_getb(c), rgba_geta(c)));
m_userDataView.entry()->setText(m_cel->data()->userData().text());
m_userDataView.textEdit()->setText(m_cel->data()->userData().text());
// Set last filled values in CelPropertiesWindow
m_lastValues.opacity = m_cel->opacity();
m_lastValues.zIndex = m_cel->zIndex();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2001-2018 David Capello
// Copyright (C) 2001-2025 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -51,7 +51,7 @@ void DuplicateLayerCommand::onExecute(Context* context)
Tx tx(writer, "Layer Duplication");
LayerImage* sourceLayer = static_cast<LayerImage*>(writer.layer());
DocApi api = document->getApi(tx);
api.duplicateLayerAfter(sourceLayer, sourceLayer->parent(), sourceLayer);
api.duplicateLayerAfter(sourceLayer, sourceLayer->parent(), sourceLayer, " Copy");
tx.commit();
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -23,7 +23,7 @@
#include "app/ui/app_menuitem.h"
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui/search_entry.h"
#include "app/ui/select_accelerator.h"
#include "app/ui/select_shortcut.h"
#include "app/ui/separator_in_view.h"
#include "app/ui/skin/skin_theme.h"
#include "base/fs.h"
@ -151,7 +151,7 @@ public:
, m_keyOrig(key ? new Key(*key) : nullptr)
, m_menuitem(menuitem)
, m_level(level)
, m_hotAccel(-1)
, m_hotShortcut(-1)
, m_lockButtons(false)
, m_headerItem(headerItem)
{
@ -204,45 +204,45 @@ public:
}
private:
void onChangeAccel(int index)
void onChangeShortcut(int index)
{
LockButtons lock(this);
Accelerator origAccel = m_key->accels()[index];
SelectAccelerator window(origAccel, m_key->keycontext(), m_keys);
Shortcut origShortcut = m_key->shortcuts()[index];
SelectShortcut window(origShortcut, m_key->keycontext(), m_keys);
window.openWindowInForeground();
if (window.isModified()) {
m_key->disableAccel(origAccel, KeySource::UserDefined);
if (!window.accel().isEmpty())
m_key->add(window.accel(), KeySource::UserDefined, m_keys);
m_key->disableShortcut(origShortcut, KeySource::UserDefined);
if (!window.shortcut().isEmpty())
m_key->add(window.shortcut(), KeySource::UserDefined, m_keys);
}
this->window()->layout();
}
void onDeleteAccel(int index)
void onDeleteShortcut(int index)
{
LockButtons lock(this);
// We need to create a copy of the accelerator because
// Key::disableAccel() will modify the accels() collection itself.
ui::Accelerator accel = m_key->accels()[index];
// We need to create a copy of the shortcut because
// Key::disableShortcut() will modify the shortcuts() collection itself.
ui::Shortcut shortcut = m_key->shortcuts()[index];
if (ui::Alert::show(Strings::alerts_delete_shortcut(accel.toString())) != 1)
if (ui::Alert::show(Strings::alerts_delete_shortcut(shortcut.toString())) != 1)
return;
m_key->disableAccel(accel, KeySource::UserDefined);
m_key->disableShortcut(shortcut, KeySource::UserDefined);
window()->layout();
}
void onAddAccel()
void onAddShortcut()
{
LockButtons lock(this);
ui::Accelerator accel;
SelectAccelerator window(accel, m_key ? m_key->keycontext() : KeyContext::Any, m_keys);
ui::Shortcut shortcut;
SelectShortcut window(shortcut, m_key ? m_key->keycontext() : KeyContext::Any, m_keys);
window.openWindowInForeground();
if ((window.isModified()) ||
// We can assign a "None" accelerator to mouse wheel actions
// We can assign a "None" shortcut to mouse wheel actions
(m_key && m_key->type() == KeyType::WheelAction && window.isOK())) {
if (!m_key) {
ASSERT(m_menuitem);
@ -256,7 +256,7 @@ private:
m_menuKeys[m_menuitem] = m_key;
}
m_key->add(window.accel(), KeySource::UserDefined, m_keys);
m_key->add(window.shortcut(), KeySource::UserDefined, m_keys);
}
this->window()->layout();
@ -273,8 +273,8 @@ private:
size.w = std::max(size.w, w);
}
if (m_key && !m_key->accels().empty()) {
size_t combos = m_key->accels().size();
if (m_key && !m_key->shortcuts().empty()) {
size_t combos = m_key->shortcuts().size();
if (combos > 1)
size.h *= combos;
}
@ -315,7 +315,7 @@ private:
}
}
if (m_key && !m_key->accels().empty()) {
if (m_key && !m_key->shortcuts().empty()) {
if (m_key->keycontext() != KeyContext::Any) {
g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()),
fg,
@ -324,13 +324,14 @@ private:
}
const int dh = th + 4 * guiscale();
IntersectClip clip(g,
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->accels().size()));
IntersectClip clip(
g,
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->shortcuts().size()));
if (clip) {
int i = 0;
for (const Accelerator& accel : m_key->accels()) {
if (i != m_hotAccel || !m_changeButton) {
g->drawText(getAccelText(accel), fg, bg, gfx::Point(keyXPos, y));
for (const Shortcut& shortcut : m_key->shortcuts()) {
if (i != m_hotShortcut || !m_changeButton) {
g->drawText(getShortcutText(shortcut), fg, bg, gfx::Point(keyXPos, y));
}
y += dh;
++i;
@ -361,40 +362,41 @@ private:
gfx::Rect bounds = this->bounds();
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
const Accelerators* accels = (m_key ? &m_key->accels() : NULL);
const Shortcuts* shortcuts = (m_key ? &m_key->shortcuts() : NULL);
int y = bounds.y;
int dh = textSize().h + 4 * guiscale();
int maxi = (accels && accels->size() > 1 ? accels->size() : 1);
int maxi = (shortcuts && shortcuts->size() > 1 ? shortcuts->size() : 1);
auto theme = SkinTheme::get(this);
auto* theme = SkinTheme::get(this);
for (int i = 0; i < maxi; ++i, y += dh) {
int w = font()->textLength(
(accels && i < (int)accels->size() ? getAccelText((*accels)[i]) : std::string()));
int w = font()->textLength((shortcuts && i < (int)shortcuts->size() ?
getShortcutText((*shortcuts)[i]) :
std::string()));
gfx::Rect itemBounds(bounds.x + m_headerItem->keyXPos(), y, w, dh);
itemBounds = itemBounds.enlarge(
gfx::Border(4 * guiscale(), 0, 6 * guiscale(), 1 * guiscale()));
if (accels && i < (int)accels->size() && mouseMsg->position().y >= itemBounds.y &&
if (shortcuts && i < (int)shortcuts->size() && mouseMsg->position().y >= itemBounds.y &&
mouseMsg->position().y < itemBounds.y + itemBounds.h) {
if (m_hotAccel != i) {
m_hotAccel = i;
if (m_hotShortcut != i) {
m_hotShortcut = i;
m_changeConn = obs::connection();
m_changeButton.reset(new Button(""));
m_changeConn = m_changeButton->Click.connect([this, i] { onChangeAccel(i); });
m_changeConn = m_changeButton->Click.connect([this, i] { onChangeShortcut(i); });
m_changeButton->setStyle(theme->styles.miniButton());
addChild(m_changeButton.get());
m_deleteConn = obs::connection();
m_deleteButton.reset(new Button(""));
m_deleteConn = m_deleteButton->Click.connect([this, i] { onDeleteAccel(i); });
m_deleteConn = m_deleteButton->Click.connect([this, i] { onDeleteShortcut(i); });
m_deleteButton->setStyle(theme->styles.miniButton());
addChild(m_deleteButton.get());
m_changeButton->setBgColor(gfx::ColorNone);
m_changeButton->setBounds(itemBounds);
m_changeButton->setText(getAccelText((*accels)[i]));
m_changeButton->setText(getShortcutText((*shortcuts)[i]));
const char* label = "x";
m_deleteButton->setBgColor(gfx::ColorNone);
@ -411,7 +413,7 @@ private:
if (i == 0 && !m_addButton && (!m_menuitem || m_menuitem->getCommand())) {
m_addConn = obs::connection();
m_addButton.reset(new Button(""));
m_addConn = m_addButton->Click.connect([this] { onAddAccel(); });
m_addConn = m_addButton->Click.connect([this] { onAddShortcut(); });
m_addButton->setStyle(theme->styles.miniButton());
addChild(m_addButton.get());
@ -452,17 +454,15 @@ private:
m_addButton->setVisible(false);
}
m_hotAccel = -1;
m_hotShortcut = -1;
}
std::string getAccelText(const Accelerator& accel) const
std::string getShortcutText(const Shortcut& shortcut) const
{
if (m_key && m_key->type() == KeyType::WheelAction && accel.isEmpty()) {
if (m_key && m_key->type() == KeyType::WheelAction && shortcut.isEmpty()) {
return Strings::keyboard_shortcuts_default_action();
}
else {
return accel.toString();
}
return shortcut.toString();
}
KeyboardShortcuts& m_keys;
@ -471,14 +471,14 @@ private:
KeyPtr m_keyOrig;
AppMenuItem* m_menuitem;
int m_level;
ui::Accelerators m_newAccels;
ui::Shortcuts m_newShortcuts;
std::shared_ptr<ui::Button> m_changeButton;
std::shared_ptr<ui::Button> m_deleteButton;
std::shared_ptr<ui::Button> m_addButton;
obs::scoped_connection m_changeConn;
obs::scoped_connection m_deleteConn;
obs::scoped_connection m_addConn;
int m_hotAccel;
int m_hotShortcut;
bool m_lockButtons;
HeaderItem* m_headerItem;
};

View File

@ -135,8 +135,16 @@ public:
remapWindow();
centerWindow();
gfx::Rect originalBounds = bounds();
load_window_pos(this, "LayerProperties");
// Queue a remap for after the user data view is configured
// if the window size has been reset and user data is visible
if (originalBounds == bounds() && Preferences::instance().layers.userDataVisibility())
m_remapAfterConfigure = true;
UIContext::instance()->add_observer(this);
}
@ -159,6 +167,11 @@ public:
if (countLayers() > 0) {
m_userDataView.configureAndSet(m_layer->userData(), g_window->propertiesGrid());
if (m_remapAfterConfigure) {
remapWindow();
centerWindow();
m_remapAfterConfigure = false;
}
}
updateFromLayer();
@ -454,7 +467,7 @@ private:
color_t c = m_layer->userData().color();
m_userDataView.color()->setColor(
Color::fromRgb(rgba_getr(c), rgba_getg(c), rgba_getb(c), rgba_geta(c)));
m_userDataView.entry()->setText(m_layer->userData().text());
m_userDataView.textEdit()->setText(m_layer->userData().text());
}
else {
name()->setText(Strings::layer_properties_no_layer());
@ -464,7 +477,7 @@ private:
m_userDataView.setVisible(false, false);
}
bool uuidVisible = m_document && m_document->sprite() && m_document->sprite()->uuidsForLayers();
bool uuidVisible = m_document && m_document->sprite() && m_document->sprite()->useLayerUuids();
uuidLabel()->setVisible(uuidVisible);
uuid()->setVisible(uuidVisible);
@ -484,6 +497,7 @@ private:
view::RealRange m_range;
bool m_selfUpdate = false;
UserDataView m_userDataView;
bool m_remapAfterConfigure = false;
};
LayerPropertiesCommand::LayerPropertiesCommand()

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -164,10 +164,10 @@ void NewBrushCommand::createBrush(const Site& site, const Mask* mask)
params.set("change", "custom");
params.set("slot", base::convert_to<std::string>(slot).c_str());
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
if (key && !key->accels().empty()) {
if (key && !key->shortcuts().empty()) {
std::string tooltip;
tooltip += Strings::new_brush_shortcut() + " ";
tooltip += key->accels().front().toString();
tooltip += key->shortcuts().front().toString();
StatusBar::instance()->showTip(2000, tooltip);
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -268,11 +268,15 @@ void NewLayerCommand::onExecute(Context* context)
switch (m_type) {
case Type::Layer:
layer = api.newLayer(parent, name);
if (m_place == Place::BeforeActiveLayer)
if (m_place == Place::BeforeActiveLayer) {
layer = api.newLayer(parent, name);
api.restackLayerBefore(layer, parent, activeLayer);
}
else
layer = api.newLayerAfter(parent, name, activeLayer);
break;
case Type::Group: layer = api.newGroup(parent, name); break;
case Type::Group: layer = api.newGroupAfter(parent, name, activeLayer); break;
case Type::ReferenceLayer:
layer = api.newLayer(parent, name);
if (layer)
@ -296,9 +300,7 @@ void NewLayerCommand::onExecute(Context* context)
tsi = tilesetInfo.tsi;
}
layer = new LayerTilemap(sprite, tsi);
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
layer = api.newTilemapAfter(parent, name, tsi, activeLayer);
break;
}
}
@ -320,10 +322,6 @@ void NewLayerCommand::onExecute(Context* context)
api.restackLayerBefore(layer, sprite->root(), first);
}
}
// Move the layer above the active one.
else if (activeLayer && m_place == Place::AfterActiveLayer) {
api.restackLayerAfter(layer, activeLayer->parent(), activeLayer);
}
// Put all selected layers inside the group
if (m_type == Type::Group && site.inTimeline()) {

View File

@ -463,11 +463,11 @@ public:
// Theme Custom Font
customThemeFont()->Click.connect([this] {
auto* theme = skin::SkinTheme::get(this);
onSwitchCustomFontCheckBox(customThemeFont(), themeFont(), theme->getOriginalDefaultFont());
onSwitchCustomFontCheckBox(customThemeFont(), themeFont(), theme->getDefaultFontInfo());
});
customMiniFont()->Click.connect([this] {
auto* theme = skin::SkinTheme::get(this);
onSwitchCustomFontCheckBox(customMiniFont(), themeMiniFont(), theme->getOriginalMiniFont());
onSwitchCustomFontCheckBox(customMiniFont(), themeMiniFont(), theme->getMiniFontInfo());
});
themeFont()->FontChange.connect([this] { updateFontPreviews(); });
themeMiniFont()->FontChange.connect([this] { updateFontPreviews(); });
@ -696,7 +696,6 @@ public:
onChangeBgScope();
onChangeGridScope();
sectionListbox()->selectIndex(m_curSection);
// Aseprite format preferences
celFormat()->setSelectedItemIndex(int(m_pref.asepriteFormat.celFormat()));
@ -1046,6 +1045,13 @@ public:
return true;
}
protected:
void onOpen(Event& evt) override
{
sectionListbox()->selectIndex(m_curSection);
app::gen::Options::onOpen(evt);
}
private:
void onInitTheme(InitThemeEvent& ev) override
{
@ -1109,14 +1115,10 @@ private:
themeMiniFont()->setInfo(miniInfo, FontEntry::From::Init);
}
void onSwitchCustomFontCheckBox(CheckBox* fontCheckBox,
FontEntry* fontEntry,
const text::FontRef& themeFont)
void onSwitchCustomFontCheckBox(CheckBox* fontCheckBox, FontEntry* fontEntry, const FontInfo& fi)
{
const bool state = fontCheckBox->isSelected();
fontEntry->setEnabled(state);
FontInfo fi = Fonts::instance()->infoFromFont(themeFont);
fontEntry->setInfo(fi, FontEntry::From::Init);
}
@ -1370,8 +1372,8 @@ private:
if (!item)
return;
const std::string lang = item->langId();
const bool state = (lang == "ar" || lang == "ja" || lang == "ko" || lang == "yue_Hant" ||
lang == "zh_Hans" || lang == "zh_Hant");
const bool state = (lang == "ar" || lang == "ja" || lang == "ko" || lang == "th" ||
lang == "yue_Hant" || lang == "zh_Hans" || lang == "zh_Hant");
fontWarningFiller()->setVisible(state);
fontWarning()->setVisible(state);
layout();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,6 +10,7 @@
#endif
#include "app/app.h"
#include "app/color_utils.h"
#include "app/commands/command.h"
#include "app/console.h"
#include "app/context.h"
@ -88,10 +89,10 @@ void PasteTextCommand::onExecute(Context* ctx)
std::string text = window.userText()->text();
app::Color color = window.fontColor()->getColor();
doc::ImageRef image = render_text(
fontInfo,
text,
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
ui::Paint paint = window.fontFace()->paint();
paint.color(color_utils::color_for_ui(color));
doc::ImageRef image = render_text(fontInfo, text, paint);
if (image) {
Sprite* sprite = editor->sprite();
if (image->pixelFormat() != sprite->pixelFormat()) {

View File

@ -142,7 +142,7 @@ public:
{
userData()->Click.connect([this] { onToggleUserData(); });
useUuidForLayers()->setSelected(sprite->uuidsForLayers());
useUuidForLayers()->setSelected(sprite->useLayerUuids());
m_userDataView.configureAndSet(m_sprite->userData(), propertiesGrid());
@ -399,7 +399,7 @@ void SpritePropertiesCommand::onExecute(Context* context)
const UserData newUserData = window.getUserData();
sprite->setUuidsForLayers(window.useUuidForLayers()->isSelected());
sprite->useLayerUuids(window.useUuidForLayers()->isSelected());
if (index != sprite->transparentColor() || pixelRatio != sprite->pixelRatio() ||
newUserData != sprite->userData()) {

View File

@ -21,6 +21,7 @@
#include "app/doc_undo.h"
#include "app/doc_undo_observer.h"
#include "app/docs_observer.h"
#include "app/i18n/strings.h"
#include "app/modules/gui.h"
#include "app/modules/palettes.h"
#include "app/site.h"
@ -292,7 +293,7 @@ public:
base::get_pretty_memory_size(static_cast<Cmd*>(state->cmd())->memSize())
#endif
:
std::string("Initial State"));
Strings::undo_history_initial_state());
if ((g->getClipBounds() & itemBounds).isEmpty())
return;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -128,7 +128,7 @@ private:
case ui::kKeyDownMessage: {
KeyboardShortcuts* keys = KeyboardShortcuts::instance();
const KeyPtr key = keys->command(CommandId::SwitchColors());
if (key && key->isPressed(msg, *keys)) {
if (key && key->isPressed(msg)) {
// Switch colors
app::Color from = m_fromButton->getColor();
app::Color to = m_toButton->getColor();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -48,9 +48,12 @@ public:
// When the main window is closed, we should close the console (in
// other case the main message loop will continue running for the
// console too).
m_mainWindowClosedConn = App::instance()->mainWindow()->Close.connect(
[this] { closeWindow(nullptr); });
// console too). The main window can be nullptr if the console is
// used to show an error when loading the default theme or font at
// the initialization.
if (auto* mainWin = App::instance()->mainWindow()) {
m_mainWindowClosedConn = mainWin->Close.connect([this] { closeWindow(nullptr); });
}
// When the window is closed, we clear the text
Close.connect([this] {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -139,6 +139,13 @@ DataRecovery::Sessions DataRecovery::sessions()
return copy;
}
bool DataRecovery::isRunningSession(const SessionPtr& session) const
{
ASSERT(session);
ASSERT(m_inProgress);
return session->path() == m_inProgress->path();
}
void DataRecovery::searchForSessions()
{
Sessions sessions;
@ -150,7 +157,7 @@ void DataRecovery::searchForSessions()
RECO_TRACE("RECO: Session '%s' ", itempath.c_str());
SessionPtr session(new Session(&m_config, itempath));
if (!session->isRunning()) {
if (!isRunningSession(session)) {
if ((session->isEmpty()) || (!session->isCrashedSession() && session->isOldSession())) {
RECO_TRACE("to be deleted (%s)\n",
session->isEmpty() ? "is empty" :

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -45,6 +45,8 @@ public:
// Returns a copy of the list of sessions that can be recovered.
Sessions sessions();
bool isRunningSession(const SessionPtr& session) const;
// Triggered in the UI-thread from the m_thread using an
// ui::execute_from_ui_thread() when the list of sessions is ready
// to be used.

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -19,12 +19,10 @@
#include "app/crash/write_document.h"
#include "app/doc.h"
#include "app/doc_access.h"
#include "app/file/file.h"
#include "app/ui_context.h"
#include "base/convert_to.h"
#include "base/fs.h"
#include "base/fstream_path.h"
#include "base/process.h"
#include "base/split_string.h"
#include "base/string.h"
#include "base/thread.h"
@ -128,12 +126,6 @@ const Session::Backups& Session::backups()
return m_backups;
}
bool Session::isRunning()
{
loadPid();
return base::get_process_name(m_pid) == base::get_process_name(base::get_current_process_id());
}
bool Session::isCrashedSession()
{
loadPid();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -47,9 +47,9 @@ public:
std::string name() const;
std::string version();
std::string& path() { return m_path; }
const Backups& backups();
bool isRunning();
bool isCrashedSession();
bool isOldSession();
bool isEmpty();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -14,6 +14,7 @@
#include "app/cmd/add_cel.h"
#include "app/cmd/add_frame.h"
#include "app/cmd/add_layer.h"
#include "app/cmd/add_tileset.h"
#include "app/cmd/clear_cel.h"
#include "app/cmd/clear_image.h"
#include "app/cmd/copy_cel.h"
@ -52,6 +53,7 @@
#include "doc/algorithm/flip_image.h"
#include "doc/algorithm/shrink_bounds.h"
#include "doc/cel.h"
#include "doc/layer_tilemap.h"
#include "doc/mask.h"
#include "doc/palette.h"
#include "doc/slice.h"
@ -605,6 +607,18 @@ LayerImage* DocApi::newLayer(LayerGroup* parent, const std::string& name)
return newLayer;
}
LayerImage* DocApi::newLayerAfter(LayerGroup* parent, const std::string& name, Layer* afterThis)
{
LayerImage* newLayer = new LayerImage(parent->sprite());
newLayer->setName(name);
if (!afterThis)
afterThis = parent->lastLayer();
addLayer(parent, newLayer, afterThis);
return newLayer;
}
LayerGroup* DocApi::newGroup(LayerGroup* parent, const std::string& name)
{
LayerGroup* newLayerGroup = new LayerGroup(parent->sprite());
@ -614,6 +628,33 @@ LayerGroup* DocApi::newGroup(LayerGroup* parent, const std::string& name)
return newLayerGroup;
}
LayerGroup* DocApi::newGroupAfter(LayerGroup* parent, const std::string& name, Layer* afterThis)
{
LayerGroup* newLayerGroup = new LayerGroup(parent->sprite());
newLayerGroup->setName(name);
if (!afterThis)
afterThis = parent->lastLayer();
addLayer(parent, newLayerGroup, afterThis);
return newLayerGroup;
}
LayerTilemap* DocApi::newTilemapAfter(LayerGroup* parent,
const std::string& name,
tileset_index tsi,
Layer* afterThis)
{
LayerTilemap* newTilemap = new LayerTilemap(parent->sprite(), tsi);
newTilemap->setName(name);
if (!afterThis)
afterThis = parent->lastLayer();
addLayer(parent, newTilemap, afterThis);
return newTilemap;
}
void DocApi::addLayer(LayerGroup* parent, Layer* newLayer, Layer* afterThis)
{
m_transaction.execute(new cmd::AddLayer(parent, newLayer, afterThis));
@ -652,23 +693,61 @@ void DocApi::restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeT
restackLayerAfter(layer, parent, afterThis);
}
Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer* afterLayer)
Layer* DocApi::copyLayerWithSprite(doc::Layer* layer, doc::Sprite* sprite)
{
std::unique_ptr<doc::Layer> clone;
if (layer->isTilemap()) {
auto* srcTilemap = static_cast<LayerTilemap*>(layer);
tileset_index tilesetIndex = srcTilemap->tilesetIndex();
// If the caller is trying to make a copy of a tilemap layer specifying a
// different sprite as its owner, then we must copy the tilesets of the
// given tilemap layer into the new owner.
if (sprite != srcTilemap->sprite()) {
auto* srcTilesetCopy = Tileset::MakeCopyCopyingImagesForSprite(srcTilemap->tileset(), sprite);
auto* addTileset = new cmd::AddTileset(sprite, srcTilesetCopy);
m_transaction.execute(addTileset);
tilesetIndex = addTileset->tilesetIndex();
}
clone = std::make_unique<LayerTilemap>(sprite, tilesetIndex);
}
else if (layer->isImage())
clone = std::make_unique<LayerImage>(sprite);
else if (layer->isGroup())
clone = std::make_unique<LayerGroup>(sprite);
else
throw std::runtime_error("Invalid layer type");
if (auto* doc = dynamic_cast<app::Doc*>(sprite->document())) {
doc->copyLayerContent(layer, doc, clone.get());
}
return clone.release();
}
Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer,
LayerGroup* parent,
Layer* afterLayer,
const std::string& nameSuffix)
{
ASSERT(parent);
Layer* newLayerPtr = copy_layer(sourceLayer);
Layer* newLayerPtr = copyLayerWithSprite(sourceLayer, parent->sprite());
newLayerPtr->setName(newLayerPtr->name() + " Copy");
newLayerPtr->setName(newLayerPtr->name() + nameSuffix);
addLayer(parent, newLayerPtr, afterLayer);
return newLayerPtr;
}
Layer* DocApi::duplicateLayerBefore(Layer* sourceLayer, LayerGroup* parent, Layer* beforeLayer)
Layer* DocApi::duplicateLayerBefore(Layer* sourceLayer,
LayerGroup* parent,
Layer* beforeLayer,
const std::string& nameSuffix)
{
ASSERT(parent);
Layer* afterThis = (beforeLayer ? beforeLayer->getPreviousBrowsable() : nullptr);
Layer* newLayer = duplicateLayerAfter(sourceLayer, parent, afterThis);
Layer* newLayer = duplicateLayerAfter(sourceLayer, parent, afterThis, nameSuffix);
if (newLayer)
restackLayerBefore(newLayer, parent, beforeLayer);
return newLayer;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -9,12 +9,14 @@
#define APP_DOC_API_H_INCLUDED
#pragma once
#include "app/doc_api_dnd_helper.h"
#include "app/drop_frame_place.h"
#include "app/tags_handling.h"
#include "doc/algorithm/flip_type.h"
#include "doc/color.h"
#include "doc/frame.h"
#include "doc/image_ref.h"
#include "doc/tile.h"
#include "gfx/rect.h"
#include <map>
@ -26,6 +28,7 @@ class Image;
class Layer;
class LayerGroup;
class LayerImage;
class LayerTilemap;
class Mask;
class Palette;
class Sprite;
@ -36,6 +39,7 @@ class Doc;
class Transaction;
using namespace doc;
using namespace docapi;
// High-level API to modify a document adding undo information, i.e.
// adding new "Cmd"s in the given transaction.
@ -104,13 +108,25 @@ public:
// Layers API
LayerImage* newLayer(LayerGroup* parent, const std::string& name);
LayerImage* newLayerAfter(LayerGroup* parent, const std::string& name, Layer* afterThis);
LayerGroup* newGroup(LayerGroup* parent, const std::string& name);
LayerGroup* newGroupAfter(LayerGroup* parent, const std::string& name, Layer* afterThis);
LayerTilemap* newTilemapAfter(LayerGroup* parent,
const std::string& name,
tileset_index tsi,
Layer* afterThis);
void addLayer(LayerGroup* parent, Layer* newLayer, Layer* afterThis);
void removeLayer(Layer* layer);
void restackLayerAfter(Layer* layer, LayerGroup* parent, Layer* afterThis);
void restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeThis);
Layer* duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer* afterLayer);
Layer* duplicateLayerBefore(Layer* sourceLayer, LayerGroup* parent, Layer* beforeLayer);
Layer* duplicateLayerAfter(Layer* sourceLayer,
LayerGroup* parent,
Layer* afterLayer,
const std::string& nameSuffix = std::string());
Layer* duplicateLayerBefore(Layer* sourceLayer,
LayerGroup* parent,
Layer* beforeLayer,
const std::string& nameSuffix = std::string());
// Images API
void replaceImage(Sprite* sprite, const ImageRef& oldImage, const ImageRef& newImage);
@ -126,6 +142,14 @@ public:
// Palette API
void setPalette(Sprite* sprite, frame_t frame, const Palette* newPalette);
// Drag and Drop helper API
void dropDocumentsOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
InsertionPoint insert,
DroppedOn droppedOn,
DocProvider& provider);
private:
void cropImageLayer(LayerImage* layer, const gfx::Rect& bounds, const bool trimOutside);
bool cropCel(LayerImage* layer, Cel* cel, const gfx::Rect& bounds, const bool trimOutside);
@ -137,6 +161,8 @@ private:
const DropFramePlace dropFramePlace,
const TagsHandling tagsHandling);
Layer* copyLayerWithSprite(doc::Layer* layer, doc::Sprite* sprite);
class HandleLinkedCels {
public:
HandleLinkedCels(DocApi& api,

View File

@ -0,0 +1,142 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/doc_api.h"
#include "app/cmd/copy_cel.h"
#include "app/cmd/set_pixel_format.h"
#include "app/cmd/set_total_frames.h"
#include "app/file/file.h"
#include "app/transaction.h"
#include "doc/layer.h"
#include "render/dithering.h"
namespace {
using namespace app;
// Returns true if the document srcDoc has a cel that can be copied into a
// destDoc's cel.
// The cel from srcDoc can be copied only when all of the following conditions
// are met:
// * Drop took place in a cel.
// * Source doc has only one layer with just one frame.
// * The layer from the source doc and the destination cel's layer are both
// Image layers.
// Otherwise this function returns false.
bool can_copy_cel(Doc* srcDoc, Doc* destDoc, DroppedOn droppedOn, doc::layer_t layerIndex)
{
auto* srcLayer = srcDoc->sprite()->firstLayer();
auto* destLayer = destDoc->sprite()->allLayers()[layerIndex];
return droppedOn == DroppedOn::Cel && srcDoc->sprite()->allLayersCount() == 1 &&
srcDoc->sprite()->totalFrames() == 1 && srcLayer->isImage() && destLayer->isImage();
}
void setup_insertion_layer(Doc* destDoc,
doc::layer_t layerIndex,
InsertionPoint& insert,
Layer*& layer,
LayerGroup*& group)
{
const LayerList& allLayers = destDoc->sprite()->allLayers();
layer = allLayers[layerIndex];
if (insert == InsertionPoint::BeforeLayer && layer->isGroup()) {
group = static_cast<LayerGroup*>(layer);
// The user is trying to drop layers into an empty group, so there is no after
// nor before layer...
if (group->layersCount() == 0) {
layer = nullptr;
return;
}
layer = group->lastLayer();
insert = InsertionPoint::AfterLayer;
}
group = layer->parent();
}
} // namespace
namespace app {
using namespace docapi;
void DocApi::dropDocumentsOnTimeline(app::Doc* destDoc,
doc::frame_t frame,
doc::layer_t layerIndex,
InsertionPoint insert,
DroppedOn droppedOn,
DocProvider& docProvider)
{
// Layer used as a reference to determine if the dropped layers will be
// inserted after or before it.
Layer* refLayer = nullptr;
// Parent group of the reference layer layer.
LayerGroup* group = nullptr;
// Keep track of the current insertion point.
setup_insertion_layer(destDoc, layerIndex, insert, refLayer, group);
int docsProcessed = 0;
while (docProvider.pendingDocs() > 0) {
std::unique_ptr<Doc> srcDoc = docProvider.nextDoc();
docsProcessed++;
// If the provider returned a null document then there was some problem with
// that specific doc but it can continue providing more documents
if (!srcDoc)
continue;
// If source document doesn't match the destination document's color
// mode, change it.
if (srcDoc->colorMode() != destDoc->colorMode()) {
// We can change the source doc pixel format out of any transaction because
// we don't need undo/redo it.
cmd::SetPixelFormat cmd(srcDoc->sprite(),
destDoc->sprite()->pixelFormat(),
render::Dithering(),
Preferences::instance().quantization.rgbmapAlgorithm(),
nullptr,
nullptr,
FitCriteria::DEFAULT);
cmd.execute(destDoc->context());
}
// If there is only one source document to process and it has a single cel
// that can be copied, then copy the cel from the source doc into the
// destination doc's selected frame.
const bool isJustOneDoc = (docsProcessed == 1 && docProvider.pendingDocs() == 0);
if (isJustOneDoc && can_copy_cel(srcDoc.get(), destDoc, droppedOn, layerIndex)) {
auto* srcLayer = static_cast<LayerImage*>(srcDoc->sprite()->firstLayer());
auto* destLayer = static_cast<LayerImage*>(destDoc->sprite()->allLayers()[layerIndex]);
m_transaction.execute(new cmd::CopyCel(srcLayer, 0, destLayer, frame, false));
break;
}
// If there is no room for the source frames, add frames to the
// destination sprite.
if (frame + srcDoc->sprite()->totalFrames() > destDoc->sprite()->totalFrames()) {
m_transaction.execute(
new cmd::SetTotalFrames(destDoc->sprite(), frame + srcDoc->sprite()->totalFrames()));
}
for (auto* srcLayer : srcDoc->sprite()->root()->layers()) {
srcLayer->displaceFrames(0, frame);
if (insert == InsertionPoint::AfterLayer) {
refLayer = duplicateLayerAfter(srcLayer, group, refLayer);
}
else {
refLayer = duplicateLayerBefore(srcLayer, group, refLayer);
insert = InsertionPoint::AfterLayer;
}
}
}
}
} // namespace app

View File

@ -0,0 +1,41 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_DOC_API_DND_HELPER_H_INCLUDED
#define APP_DOC_API_DND_HELPER_H_INCLUDED
#pragma once
#include "app/doc.h"
namespace app { namespace docapi {
enum class InsertionPoint {
BeforeLayer,
AfterLayer,
};
enum class DroppedOn {
Unspecified,
Frame,
Layer,
Cel,
};
// Used by the drag & drop helper API as a provider of the documents that
// were dragged and then dropped in some Drag&Drop aware widget.
class DocProvider {
public:
virtual ~DocProvider() = default;
// Returns the next document from the underlying provider's implementation,
// returns null if there is no more documents to provide.
virtual std::unique_ptr<Doc> nextDoc() = 0;
// Returns the number of docs that were not provided yet.
virtual int pendingDocs() = 0;
};
}} // namespace app::docapi
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -43,6 +43,7 @@
#include "gfx/size.h"
#include "render/dithering.h"
#include "render/ordered_dither.h"
#include "render/quantization.h"
#include "render/render.h"
#include "ver/info.h"
@ -69,6 +70,104 @@ std::string escape_for_json(const std::string& path)
return res;
}
// Forward declaration
void serialize_properties(const doc::UserData::Properties& props, std::ostream& os);
// Helper for a single value
void serialize_variant(const doc::UserData::Variant& value, std::ostream& os)
{
using Properties = doc::UserData::Properties;
switch (value.index()) {
case USER_DATA_PROPERTY_TYPE_BOOL: os << (get_value<bool>(value) ? "true" : "false"); break;
case USER_DATA_PROPERTY_TYPE_INT8: os << static_cast<int64_t>(get_value<int8_t>(value)); break;
case USER_DATA_PROPERTY_TYPE_UINT8:
os << static_cast<int64_t>(get_value<uint8_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_INT16:
os << static_cast<int64_t>(get_value<int16_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_UINT16:
os << static_cast<int64_t>(get_value<uint16_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_INT32:
os << static_cast<int64_t>(get_value<int32_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_UINT32:
os << static_cast<int64_t>(get_value<uint32_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_INT64:
os << static_cast<int64_t>(get_value<int64_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_UINT64:
os << static_cast<int64_t>(get_value<uint64_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_FLOAT: os << get_value<float>(value); break;
case USER_DATA_PROPERTY_TYPE_DOUBLE: os << get_value<double>(value); break;
case USER_DATA_PROPERTY_TYPE_STRING:
os << "\"" << escape_for_json(get_value<std::string>(value)) << "\"";
break;
case USER_DATA_PROPERTY_TYPE_PROPERTIES:
serialize_properties(get_value<Properties>(value), os);
break;
default: os << "\"[unsupported type]\""; break;
}
}
// Serializes a map of properties
void serialize_properties(const doc::UserData::Properties& props, std::ostream& os)
{
os << "{";
bool first = true;
for (const auto& [key, value] : props) {
if (!first)
os << ", ";
first = false;
os << "\"" << escape_for_json(key) << "\": ";
serialize_variant(value, os);
}
os << "}";
}
void serialize_userdata_properties(const doc::UserData& data, std::ostream& os)
{
const auto& propsMaps = data.propertiesMaps();
bool hasAnyProps = false;
for (const auto& [group, props] : propsMaps) {
if (!props.empty()) {
hasAnyProps = true;
break;
}
}
if (hasAnyProps) {
os << ", \"properties\": {";
bool firstProp = true;
for (const auto& [group, props] : propsMaps) {
if (!props.empty()) {
if (!firstProp)
os << ", ";
firstProp = false;
if (group.empty()) {
// Default group: flatten its keys at the top level
bool firstKey = true;
for (const auto& [key, value] : props) {
if (!firstKey)
os << ", ";
firstKey = false;
os << "\"" << escape_for_json(key) << "\": ";
serialize_variant(value, os);
}
}
else {
// Named group: nest under its group name
os << "\"" << escape_for_json(group) << "\": ";
serialize_properties(props, os);
}
}
}
os << "}";
}
}
std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
{
doc::color_t color = data.color();
@ -80,6 +179,9 @@ std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
}
if (!data.text().empty())
os << ", \"data\": \"" << escape_for_json(data.text()) << "\"";
serialize_userdata_properties(data, os);
return os;
}
@ -300,6 +402,35 @@ public:
}
}
void setPixelFormat(const doc::PixelFormat newPixelFormat)
{
if (!m_image || m_image->pixelFormat() == newPixelFormat)
return;
ImageSpec spec(ColorMode(newPixelFormat),
m_image->width(),
m_image->height(),
m_image->maskColor());
ImageRef convertedImg(Image::create(spec));
if (!convertedImg)
return;
clear_image(convertedImg.get(), 0);
render::Dithering dithering;
Sprite* sprite = this->sprite();
render::convert_pixel_format(m_image.get(),
convertedImg.get(),
newPixelFormat,
dithering,
sprite ? sprite->rgbMap(0) : nullptr,
sprite ? sprite->palette(0) : nullptr,
(sprite && sprite->backgroundLayer()),
0,
0,
nullptr);
m_image = convertedImg;
}
private:
Doc* m_document;
Sprite* m_sprite;
@ -679,6 +810,9 @@ Doc* DocExporter::exportSheet(Context* ctx, base::task_token& token)
Sprite* texture = textureDocument->sprite();
Image* textureImage = texture->root()->firstLayer()->cel(frame_t(0))->image();
for (auto& sample : samples)
sample.setPixelFormat(texture->pixelFormat());
renderTexture(ctx, samples, textureImage, token);
if (token.canceled())
return nullptr;
@ -1245,22 +1379,6 @@ void DocExporter::renderTexture(Context* ctx,
++i;
continue;
}
// Make the sprite compatible with the texture so the render()
// works correctly.
if (sample.sprite()->pixelFormat() != textureImage->pixelFormat()) {
RgbMapAlgorithm rgbmapAlgo = Preferences::instance().quantization.rgbmapAlgorithm();
FitCriteria fc = Preferences::instance().quantization.fitCriteria();
cmd::SetPixelFormat(sample.sprite(),
textureImage->pixelFormat(),
render::Dithering(),
rgbmapAlgo,
nullptr, // toGray is not needed because the texture is Indexed or RGB
nullptr, // TODO add a delegate to show progress
fc)
.execute(ctx);
}
sample.renderSample(textureImage,
sample.inTextureBounds().x + m_innerPadding,
sample.inTextureBounds().y + m_innerPadding,

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -405,7 +405,8 @@ static DocRange drop_range_op(Doc* doc,
if (place == kDocRangeBefore) {
Layer* beforeThis = (!dstLayers.empty() ? dstLayers.front() : nullptr);
for (Layer* srcLayer : srcLayers) {
Layer* copiedLayer = api.duplicateLayerBefore(srcLayer, parent, beforeThis);
Layer* copiedLayer =
api.duplicateLayerBefore(srcLayer, parent, beforeThis, " Copy");
resultRange.startRange(copiedLayer, -1, DocRange::kLayers);
resultRange.endRange(copiedLayer, -1);
@ -416,7 +417,7 @@ static DocRange drop_range_op(Doc* doc,
Layer* afterThis = (!dstLayers.empty() ? dstLayers.back() : nullptr);
for (Layer* srcLayer : srcLayers) {
Layer* copiedLayer = api.duplicateLayerAfter(srcLayer, parent, afterThis);
Layer* copiedLayer = api.duplicateLayerAfter(srcLayer, parent, afterThis, " Copy");
resultRange.startRange(copiedLayer, -1, DocRange::kLayers);
resultRange.endRange(copiedLayer, -1);

View File

@ -544,7 +544,7 @@ static void ase_file_prepare_header(FILE* f,
0);
header->flags = (ASE_FILE_FLAG_LAYER_WITH_OPACITY |
(composeGroups ? ASE_FILE_FLAG_COMPOSITE_GROUPS : 0) |
(sprite->uuidsForLayers() ? ASE_FILE_FLAG_LAYER_WITH_UUID : 0));
(sprite->useLayerUuids() ? ASE_FILE_FLAG_LAYER_WITH_UUID : 0));
header->speed = sprite->frameDuration(firstFrame);
header->next = 0;
header->frit = 0;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -1401,7 +1401,7 @@ private:
color_t color = *srcIt;
int i;
if (rgba_geta(color) >= 128) {
if (rgba_geta(color) > 0) {
i = framePalette.findExactMatch(rgba_getr(color),
rgba_getg(color),
rgba_getb(color),

View File

@ -75,20 +75,86 @@ bool SvgFormat::onSave(FileOp* fop)
const int pixelScaleValue = std::clamp(svg_options->pixelScale, 0, 10000);
FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
FILE* f = handle.get();
auto printcol = [f](int x, int y, int r, int g, int b, int a, int pxScale) {
fprintf(f,
"<rect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"#%02X%02X%02X\" ",
x * pxScale,
y * pxScale,
pxScale,
pxScale,
r,
g,
b);
if (a != 255)
fprintf(f, "opacity=\"%f\" ", (float)a / 255.0);
fprintf(f, "/>\n");
auto printRect =
[f](int x, int y, int width, int height, int r, int g, int b, int a, int pxScale) {
fprintf(f,
"<rect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"#%02X%02X%02X\" ",
x * pxScale,
y * pxScale,
width * pxScale,
height * pxScale,
r,
g,
b);
if (a != 255)
fprintf(f, "opacity=\"%f\" ", (float)a / 255.0);
fprintf(f, "/>\n");
};
// Helper function for greedy merge of same-colored pixels
auto mergePixels = [&](auto getPixelColor) {
std::vector<std::vector<bool>> processed(image->height(),
std::vector<bool>(image->width(), false));
for (y = 0; y < image->height(); y++) {
for (x = 0; x < image->width(); x++) {
if (processed[y][x])
continue;
auto [pixelR, pixelG, pixelB, pixelA, shouldSkip] = getPixelColor(x, y);
if (shouldSkip) {
processed[y][x] = true;
continue;
}
// Find the largest rectangle starting from this pixel
int maxWidth = 0;
int maxHeight = 0;
// Find maximum width for current row
for (int w = x; w < image->width(); w++) {
if (processed[y][w])
break;
auto [checkR, checkG, checkB, checkA, skip] = getPixelColor(w, y);
if (skip || checkR != pixelR || checkG != pixelG || checkB != pixelB || checkA != pixelA)
break;
maxWidth = w - x + 1;
}
// Find maximum height with current width
for (int h = y; h < image->height(); h++) {
bool canExtend = true;
for (int w = x; w < x + maxWidth; w++) {
if (processed[h][w]) {
canExtend = false;
break;
}
auto [checkR, checkG, checkB, checkA, skip] = getPixelColor(w, h);
if (skip || checkR != pixelR || checkG != pixelG || checkB != pixelB ||
checkA != pixelA) {
canExtend = false;
break;
}
}
if (!canExtend)
break;
maxHeight = h - y + 1;
}
// Mark rectangle as processed and output it
for (int h = y; h < y + maxHeight; h++) {
for (int w = x; w < x + maxWidth; w++) {
processed[h][w] = true;
}
}
printRect(x, y, maxWidth, maxHeight, pixelR, pixelG, pixelB, pixelA, pixelScaleValue);
}
fop->setProgress((float)y / (float)(image->height()));
}
};
fprintf(f, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
fprintf(
f,
@ -98,59 +164,55 @@ bool SvgFormat::onSave(FileOp* fop)
switch (image->pixelFormat()) {
case IMAGE_RGB: {
for (y = 0; y < image->height(); y++) {
for (x = 0; x < image->width(); x++) {
c = get_pixel_fast<RgbTraits>(image.get(), x, y);
alpha = rgba_geta(c);
if (alpha != 0x00)
printcol(x, y, rgba_getr(c), rgba_getg(c), rgba_getb(c), alpha, pixelScaleValue);
}
fop->setProgress((float)y / (float)(image->height()));
}
auto getPixelColor = [&](int px, int py) -> std::tuple<int, int, int, int, bool> {
c = get_pixel_fast<RgbTraits>(image.get(), px, py);
alpha = rgba_geta(c);
if (alpha == 0x00)
return { 0, 0, 0, 0, true };
return { rgba_getr(c), rgba_getg(c), rgba_getb(c), alpha, false };
};
mergePixels(getPixelColor);
break;
}
case IMAGE_GRAYSCALE: {
for (y = 0; y < image->height(); y++) {
for (x = 0; x < image->width(); x++) {
c = get_pixel_fast<GrayscaleTraits>(image.get(), x, y);
auto v = graya_getv(c);
alpha = graya_geta(c);
if (alpha != 0x00)
printcol(x, y, v, v, v, alpha, pixelScaleValue);
}
fop->setProgress((float)y / (float)(image->height()));
}
auto getPixelColor = [&](int px, int py) -> std::tuple<int, int, int, int, bool> {
c = get_pixel_fast<GrayscaleTraits>(image.get(), px, py);
auto v = graya_getv(c);
alpha = graya_geta(c);
if (alpha == 0x00)
return { 0, 0, 0, 0, true };
return { v, v, v, alpha, false };
};
mergePixels(getPixelColor);
break;
}
case IMAGE_INDEXED: {
unsigned char image_palette[256][4];
for (y = 0; y < 256; y++) {
fop->sequenceGetColor(y, &r, &g, &b);
image_palette[y][0] = r;
image_palette[y][1] = g;
image_palette[y][2] = b;
fop->sequenceGetAlpha(y, &a);
image_palette[y][3] = a;
for (int i = 0; i < 256; i++) {
fop->sequenceGetColor(i, &r, &g, &b);
image_palette[i][0] = r;
image_palette[i][1] = g;
image_palette[i][2] = b;
fop->sequenceGetAlpha(i, &a);
image_palette[i][3] = a;
}
color_t mask_color = -1;
if (fop->document()->sprite()->backgroundLayer() == NULL ||
!fop->document()->sprite()->backgroundLayer()->isVisible()) {
mask_color = fop->document()->sprite()->transparentColor();
}
for (y = 0; y < image->height(); y++) {
for (x = 0; x < image->width(); x++) {
c = get_pixel_fast<IndexedTraits>(image.get(), x, y);
if (c != mask_color)
printcol(x,
y,
image_palette[c][0] & 0xff,
image_palette[c][1] & 0xff,
image_palette[c][2] & 0xff,
image_palette[c][3] & 0xff,
pixelScaleValue);
}
fop->setProgress((float)y / (float)(image->height()));
}
auto getPixelColor = [&](int px, int py) -> std::tuple<int, int, int, int, bool> {
c = get_pixel_fast<IndexedTraits>(image.get(), px, py);
if (c == mask_color)
return { 0, 0, 0, 0, true };
return { image_palette[c][0] & 0xff,
image_palette[c][1] & 0xff,
image_palette[c][2] & 0xff,
image_palette[c][3] & 0xff,
false };
};
mergePixels(getPixelColor);
break;
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -82,6 +82,9 @@ public:
unsigned int m_version;
bool m_removed;
mutable bool m_is_folder;
#ifdef _WIN32
bool m_isHidden = false;
#endif
std::atomic<double> m_thumbnailProgress;
std::atomic<os::Surface*> m_thumbnail;
#ifdef _WIN32
@ -266,7 +269,7 @@ IFileItem* FileSystemModule::getRootFileItem()
fileitem->m_pidl = pidl;
fileitem->m_fullpidl = pidl;
SFGAOF attrib = SFGAO_FOLDER;
SFGAOF attrib = SFGAO_FOLDER | SFGAO_HIDDEN;
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
update_by_pidl(fileitem, attrib);
@ -357,7 +360,7 @@ bool FileItem::isHidden() const
ASSERT(m_displayname != NOTINITIALIZED);
#ifdef _WIN32
return false;
return m_isHidden;
#else
return m_displayname[0] == '.';
#endif
@ -462,7 +465,7 @@ const FileItemList& FileItem::children()
// Get the interface to enumerate subitems
hr = pFolder->EnumObjects(
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN,
&pEnum);
if (hr == S_OK && pEnum) {
@ -473,10 +476,9 @@ const FileItemList& FileItem::children()
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
// Request the SFGAO_FOLDER attribute to know what of the
// item is file or a folder
for (c = 0; c < fetched; ++c) {
attribs[c] = SFGAO_FOLDER;
pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)itempidl, attribs + c);
}
for (c = 0; c < fetched; ++c)
attribs[c] = SFGAO_FOLDER | SFGAO_HIDDEN;
pFolder->GetAttributesOf(fetched, (LPCITEMIDLIST*)itempidl, attribs);
// Generate the FileItems
for (c = 0; c < fetched; ++c) {
@ -755,6 +757,9 @@ static void update_by_pidl(FileItem* fileitem, SFGAOF attrib)
// Is it a folder?
fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);
#if _WIN32
fileitem->m_isHidden = (attrib & SFGAO_HIDDEN ? true : false);
#endif
// Get the name to display

View File

@ -14,17 +14,25 @@
#include "text/font.h"
#include "text/font_mgr.h"
#include "text/sprite_sheet_font.h"
#include "text/typeface.h"
#include "ui/manager.h"
#include "ui/scale.h"
#include <set>
#define USE_CACHE 1
namespace app {
FontData::FontData(text::FontType type)
: m_type(type)
, m_antialias(false)
, m_fallback(nullptr)
, m_fallbackSize(0)
FontData::FontData(text::FontType type) : m_type(type)
{
}
FontData::FontData(const text::FontRef& nativeFont)
: m_type(nativeFont->type())
, m_antialias(nativeFont->antialias())
, m_hinting(nativeFont->hinting())
, m_nativeFont(nativeFont)
{
}
@ -32,17 +40,16 @@ text::FontRef FontData::getFont(text::FontMgrRef& fontMgr, float size)
{
ASSERT(fontMgr);
// Use cache
if (m_antialias) {
auto it = m_antialiasFonts.find(size);
if (it != m_antialiasFonts.end())
return it->second;
}
else {
auto it = m_fonts.find(size);
if (it != m_fonts.end())
return it->second;
}
if (size == 0.0f && m_size != 0.0f)
size = m_size;
#if USE_CACHE
// Use cached fonts
const Cache::Key cacheKey{ size, m_antialias, m_hinting != text::FontHinting::None };
auto it = m_cache.fonts.find(cacheKey);
if (it != m_cache.fonts.end()) // Cache hit
return it->second;
#endif
text::FontRef font = nullptr;
@ -53,27 +60,41 @@ text::FontRef FontData::getFont(text::FontMgrRef& fontMgr, float size)
static_cast<text::SpriteSheetFont*>(font.get())->setDescent(m_descent);
}
break;
case text::FontType::FreeType: {
font = fontMgr->loadTrueTypeFont(m_filename.c_str(), size);
if (font)
if (font) {
font->setAntialias(m_antialias);
font->setHinting(m_hinting);
}
break;
}
case text::FontType::Native:
if (size == m_nativeFont->size())
font = m_nativeFont;
else {
text::TypefaceRef typeface = m_nativeFont->typeface();
font = fontMgr->makeFont(typeface, size);
font->setAntialias(m_antialias);
font->setHinting(m_hinting);
}
break;
}
#if USE_CACHE
// Cache this font
m_cache.fonts[cacheKey] = font;
#endif
// Load fallback
if (m_fallback) {
text::FontRef fallback = m_fallback->getFont(fontMgr, m_fallbackSize);
text::FontRef fallback = m_fallback->getFont(fontMgr, m_fallbackSize * ui::guiscale());
if (font)
font->setFallback(fallback.get());
font->setFallback(fallback);
else
return fallback; // Don't double-cache the fallback font
}
// Cache this font
if (m_antialias)
m_antialiasFonts[size] = font;
else
m_fonts[size] = font;
return font;
}

View File

@ -22,9 +22,20 @@ namespace app {
class FontData {
public:
FontData(text::FontType type);
FontData(const text::FontRef& nativeFont);
text::FontType type() const { return m_type; }
const std::string& name() const { return m_name; }
const std::string& filename() const { return m_filename; }
float defaultSize() const { return m_size; }
bool antialias() const { return m_antialias; }
text::FontHinting hinting() const { return m_hinting; }
void setName(const std::string& name) { m_name = name; }
void setFilename(const std::string& filename) { m_filename = filename; }
void setAntialias(bool antialias) { m_antialias = antialias; }
void setDefaultSize(const float size) { m_size = size; }
void setAntialias(const bool antialias) { m_antialias = antialias; }
void setHinting(const text::FontHinting hinting) { m_hinting = hinting; }
void setFallback(FontData* fallback, float fallbackSize)
{
m_fallback = fallback;
@ -36,17 +47,32 @@ public:
text::FontRef getFont(text::FontMgrRef& fontMgr, float size);
const std::string& filename() const { return m_filename; }
private:
// Cache of loaded fonts so we avoid re-loading them.
struct Cache {
struct Key {
float size;
bool antialias : 1;
bool hinting : 1;
bool operator<(const Key& b) const
{
return size < b.size || antialias < b.antialias || hinting < b.hinting;
}
};
std::map<Key, text::FontRef> fonts;
};
text::FontType m_type;
std::string m_name;
std::string m_filename;
bool m_antialias;
std::map<float, text::FontRef> m_fonts; // key=font size, value=real font
std::map<float, text::FontRef> m_antialiasFonts;
FontData* m_fallback;
float m_fallbackSize;
float m_size = 0.0f;
bool m_antialias = false;
text::FontHinting m_hinting = text::FontHinting::Normal;
Cache m_cache;
FontData* m_fallback = nullptr;
float m_fallbackSize = 0.0f;
float m_descent = 0.0f;
text::FontRef m_nativeFont;
DISABLE_COPYING(FontData);
};

View File

@ -10,6 +10,7 @@
#include "app/fonts/font_info.h"
#include "app/fonts/font_data.h"
#include "app/pref/preferences.h"
#include "base/fs.h"
#include "base/split_string.h"
@ -27,27 +28,46 @@ FontInfo::FontInfo(Type type,
const std::string& name,
const float size,
const text::FontStyle style,
const Flags flags)
const Flags flags,
const text::FontHinting hinting)
: m_type(type)
, m_name(name)
, m_size(size)
, m_style(style)
, m_flags(flags)
, m_hinting(hinting)
{
}
FontInfo::FontInfo(const FontInfo& other,
const float size,
const text::FontStyle style,
const Flags flags)
const Flags flags,
text::FontHinting hinting)
: m_type(other.type())
, m_name(other.name())
, m_size(size)
, m_style(style)
, m_flags(flags)
, m_hinting(hinting)
{
}
FontInfo::FontInfo(const FontData* data, const float size)
: m_type(Type::Unknown)
, m_name(data->name())
, m_size(size != 0.0f ? size : data->defaultSize())
, m_flags(data->antialias() ? Flags::Antialias : Flags::None)
, m_hinting(data->hinting())
{
switch (data->type()) {
case text::FontType::Unknown: m_type = Type::Unknown; break;
case text::FontType::SpriteSheet:
case text::FontType::FreeType: m_type = Type::File; break;
case text::FontType::Native: m_type = Type::System; break;
}
}
std::string FontInfo::title() const
{
return m_type == FontInfo::Type::File ? base::get_file_name(m_name) : m_name;
@ -130,6 +150,12 @@ std::string FontInfo::humanString() const
result += " Antialias";
if (ligatures())
result += " Ligatures";
switch (hinting()) {
case text::FontHinting::None: result += " No Hinting"; break;
case text::FontHinting::Slight: result += " Slight Hinting"; break;
case text::FontHinting::Normal: break;
case text::FontHinting::Full: result += " Full Hinting"; break;
}
}
return result;
}
@ -150,6 +176,7 @@ app::FontInfo convert_to(const std::string& from)
bool bold = false;
bool italic = false;
app::FontInfo::Flags flags = app::FontInfo::Flags::None;
text::FontHinting hinting = text::FontHinting::Normal;
if (!parts.empty()) {
if (parts[0].compare(0, 5, "file=") == 0) {
@ -176,6 +203,17 @@ app::FontInfo convert_to(const std::string& from)
else if (parts[i].compare(0, 5, "size=") == 0) {
size = std::strtof(parts[i].substr(5).c_str(), nullptr);
}
else if (parts[i].compare(0, 8, "hinting=") == 0) {
std::string hintingStr = parts[i].substr(8);
if (hintingStr == "none")
hinting = text::FontHinting::None;
else if (hintingStr == "slight")
hinting = text::FontHinting::Slight;
else if (hintingStr == "normal")
hinting = text::FontHinting::Normal;
else if (hintingStr == "full")
hinting = text::FontHinting::Full;
}
}
}
@ -187,7 +225,7 @@ app::FontInfo convert_to(const std::string& from)
else if (italic)
style = text::FontStyle::Italic();
return app::FontInfo(type, name, size, style, flags);
return app::FontInfo(type, name, size, style, flags, hinting);
}
template<>
@ -213,6 +251,17 @@ std::string convert_to(const app::FontInfo& from)
result += ",antialias";
if (from.ligatures())
result += ",ligatures";
if (from.hinting() != text::FontHinting::Normal) {
result += ",hinting=";
switch (from.hinting()) {
case text::FontHinting::None: result += "none"; break;
case text::FontHinting::Slight: result += "slight"; break;
case text::FontHinting::Normal:
// Filtered out by above if
break;
case text::FontHinting::Full: result += "full"; break;
}
}
}
return result;
}

View File

@ -10,6 +10,7 @@
#include "base/convert_to.h"
#include "base/enum_flags.h"
#include "text/font_hinting.h"
#include "text/font_style.h"
#include "text/font_type.h"
#include "text/fwd.h"
@ -20,6 +21,8 @@
namespace app {
class FontData;
// Represents a font reference from any place:
// - Name: a font referenced by name, a font that came from fonts.xml files (Fonts/FontData)
// - File: an external font loaded from a .ttf file
@ -49,9 +52,16 @@ public:
const std::string& name = {},
float size = kDefaultSize,
text::FontStyle style = text::FontStyle(),
Flags flags = Flags::None);
Flags flags = Flags::None,
text::FontHinting hinting = text::FontHinting::Normal);
FontInfo(const FontInfo& other, float size, text::FontStyle style, Flags flags);
FontInfo(const FontInfo& other,
float size,
text::FontStyle style,
Flags flags,
text::FontHinting hinting);
FontInfo(const FontData* data, float size = 0.0f);
bool isValid() const { return m_type != Type::Unknown; }
bool useDefaultSize() const { return m_size == kDefaultSize; }
@ -72,6 +82,7 @@ public:
Flags flags() const { return m_flags; }
bool antialias() const;
bool ligatures() const;
text::FontHinting hinting() const { return m_hinting; }
text::TypefaceRef findTypeface(const text::FontMgrRef& fontMgr) const;
@ -92,6 +103,7 @@ private:
float m_size = kDefaultSize;
text::FontStyle m_style;
Flags m_flags = Flags::None;
text::FontHinting m_hinting = text::FontHinting::Normal;
};
LAF_ENUM_FLAGS(FontInfo::Flags);

View File

@ -37,8 +37,10 @@ Fonts::~Fonts()
g_instance = nullptr;
}
void Fonts::addFontData(const std::string& name, std::unique_ptr<FontData>&& fontData)
void Fonts::addFontData(std::unique_ptr<FontData>&& fontData)
{
ASSERT(fontData);
std::string name = fontData->name();
m_fonts[name] = std::move(fontData);
}
@ -68,10 +70,13 @@ text::FontRef Fonts::fontFromInfo(const FontInfo& fontInfo)
if (fontInfo.type() == FontInfo::Type::System) {
// Just in case the typeface is not present in the FontInfo
auto typeface = fontInfo.findTypeface(m_fontMgr);
if (!typeface)
return nullptr;
font = m_fontMgr->makeFont(typeface);
if (!fontInfo.useDefaultSize())
font->setSize(fontInfo.size());
if (fontInfo.useDefaultSize())
font = m_fontMgr->makeFont(typeface);
else
font = m_fontMgr->makeFont(typeface, fontInfo.size());
}
else {
const float size = (fontInfo.useDefaultSize() ? 0.0f : fontInfo.size());
@ -82,24 +87,12 @@ text::FontRef Fonts::fontFromInfo(const FontInfo& fontInfo)
}
}
if (font)
if (font) {
font->setAntialias(fontInfo.antialias());
font->setHinting(fontInfo.hinting());
}
return font;
}
FontInfo Fonts::infoFromFont(const text::FontRef& font)
{
for (const auto& kv : m_fonts) {
if (kv.second->getFont(m_fontMgr, font->size()) == font) {
return FontInfo(FontInfo::Type::Name,
kv.first,
font->size(),
text::FontStyle(),
font->antialias() ? FontInfo::Flags::Antialias : FontInfo::Flags::None);
}
}
return {};
}
} // namespace app

View File

@ -33,12 +33,11 @@ public:
const FontDataMap& definedFonts() const { return m_fonts; }
bool isEmpty() const { return m_fonts.empty(); }
void addFontData(const std::string& name, std::unique_ptr<FontData>&& fontData);
void addFontData(std::unique_ptr<FontData>&& fontData);
FontData* fontDataByName(const std::string& name);
text::FontRef fontByName(const std::string& name, float size);
text::FontRef fontFromInfo(const FontInfo& fontInfo);
FontInfo infoFromFont(const text::FontRef& font);
private:
text::FontMgrRef m_fontMgr;

View File

@ -682,11 +682,11 @@ bool CustomizedGuiManager::processKey(Message* msg)
{
App* app = App::instance();
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
const KeyContext contexts[] = { keys->getCurrentKeyContext(), KeyContext::Normal };
const KeyContext contexts[] = { KeyboardShortcuts::getCurrentKeyContext(), KeyContext::Normal };
int n = (contexts[0] != contexts[1] ? 2 : 1);
for (int i = 0; i < n; ++i) {
for (const KeyPtr& key : *keys) {
if (key->isPressed(msg, *keys, contexts[i])) {
if (key->isPressed(msg, contexts[i])) {
// Cancel menu-bar loops (to close any popup menu)
app->mainWindow()->getMenuBar()->cancelMenuLoop();
@ -700,7 +700,7 @@ bool CustomizedGuiManager::processKey(Message* msg)
// Collect all tools with the pressed keyboard-shortcut
for (tools::Tool* tool : *toolbox) {
const KeyPtr key = keys->tool(tool);
if (key && key->isPressed(msg, *keys))
if (key && key->isPressed(msg))
possibles.push_back(tool);
}

View File

@ -213,7 +213,7 @@ void ResourceFinder::includeDesktopDir(const char* filename)
#ifdef _WIN32
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) {
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
// released Aseprite versions.
#define API_VERSION 32
#define API_VERSION 34
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -575,8 +575,9 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
bool vexpand = (widget->type() == Canvas::Type());
// This is to separate different kind of widgets without label in
// different rows.
if (dlg->lastWidgetType != widget->type() || dlg->autoNewRow) {
// different rows. Separator widgets will always create a new row.
if (dlg->lastWidgetType != widget->type() || dlg->autoNewRow ||
widget->type() == ui::kSeparatorWidget) {
dlg->lastWidgetType = widget->type();
dlg->hbox = nullptr;
}
@ -631,8 +632,8 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
dlg->labelWidgets[id] = labelWidget;
}
else {
// For tabs we don't want the empty space of an unspecified label.
if (widget->type() != Tabs::Type()) {
// For tabs and separators, we don't want the empty space of an unspecified label.
if (widget->type() != Tabs::Type() && widget->type() != ui::kSeparatorWidget) {
dlg->currentGrid->addChildInCell(new ui::HBox, 1, 1, ui::LEFT | ui::TOP);
}
}
@ -641,14 +642,15 @@ int Dialog_add_widget(lua_State* L, Widget* widget)
if (widget->type() == ui::kButtonWidget)
hbox->enableFlags(ui::HOMOGENEOUS);
// For tabs we don't want the empty space of an unspecified label, so
// For tabs and unlabeled separators, we don't want the empty space of an unspecified label, so
// span 2 columns.
const int hspan = (widget->type() == Tabs::Type() ? 2 : 1);
const int hspan =
((widget->type() == Tabs::Type()) || (widget->type() == ui::kSeparatorWidget && !label) ? 2 :
1);
dlg->currentGrid->addChildInCell(hbox,
hspan,
1,
ui::HORIZONTAL | (vexpand ? ui::VERTICAL : ui::TOP));
dlg->hbox = hbox;
}
@ -709,12 +711,7 @@ int Dialog_separator(lua_State* L)
dlg->dataWidgets[id] = widget;
}
dlg->mainWidgets.push_back(widget);
dlg->currentGrid->addChildInCell(widget, 2, 1, ui::HORIZONTAL | ui::TOP);
dlg->hbox = nullptr;
lua_pushvalue(L, 1);
return 1;
return Dialog_add_widget(L, widget);
}
int Dialog_label(lua_State* L)
@ -1048,7 +1045,7 @@ int Dialog_shades(lua_State* L)
int Dialog_file(lua_State* L)
{
std::string title = "Open File";
std::string path = std::string();
std::string path;
std::string fn;
base::paths exts;
auto dlgType = FileSelectorType::Open;
@ -1114,11 +1111,14 @@ int Dialog_file(lua_State* L)
// Set default path if 'basepath' is blank
if (path.empty()) {
const auto* doc = App::instance()->context()->activeDocument();
if (doc)
path = base::get_file_path(doc->filename());
else
path = (base::get_file_path(fn).empty() ? base::get_current_path() : base::get_file_path(fn));
// We use the 'filename' path the relative path if it was given.
path = base::get_file_path(fn);
if (path.empty()) {
if (const auto* doc = App::instance()->context()->activeDocument())
path = base::get_file_path(doc->filename());
else
path = base::get_current_path();
}
}
// Update the widget with the provided filename

View File

@ -370,6 +370,10 @@ public:
#if ENABLE_REMAP_TILESET_EVENT
RemapTileset,
#endif
LayerBlendMode,
LayerName,
LayerOpacity,
LayerVisibility,
};
SpriteEvents(const Sprite* sprite) : m_spriteId(sprite->id()) { doc()->add_observer(this); }
@ -401,6 +405,14 @@ public:
else if (std::strcmp(eventName, "remaptileset") == 0)
return RemapTileset;
#endif
else if (std::strcmp(eventName, "layerblendmode") == 0)
return LayerBlendMode;
else if (std::strcmp(eventName, "layername") == 0)
return LayerName;
else if (std::strcmp(eventName, "layeropacity") == 0)
return LayerOpacity;
else if (std::strcmp(eventName, "layervisibility") == 0)
return LayerVisibility;
else
return Unknown;
}
@ -460,6 +472,38 @@ public:
});
}
void onLayerBlendModeChange(DocEvent& ev) override
{
call(LayerBlendMode,
{
{ "layer", ev.layer() }
});
}
void onLayerNameChange(DocEvent& ev) override
{
call(LayerName,
{
{ "layer", ev.layer() }
});
}
void onLayerOpacityChange(DocEvent& ev) override
{
call(LayerOpacity,
{
{ "layer", ev.layer() }
});
}
void onAfterLayerVisibilityChange(DocEvent& ev) override
{
call(LayerVisibility,
{
{ "layer", ev.layer() }
});
}
private:
void onAddFirstListener(EventType eventType) override
{

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2015-2018 David Capello
//
// This program is distributed under the terms of
@ -9,6 +9,7 @@
#include "config.h"
#endif
#include "app/cmd/clear_rect.h"
#include "app/cmd/copy_rect.h"
#include "app/cmd/copy_region.h"
#include "app/cmd/flip_image.h"
@ -246,7 +247,15 @@ int Image_clear(lua_State* L)
else
color = convert_args_into_pixel_color(L, i, img->pixelFormat());
doc::fill_rect(img, rc, color); // Clips the rectangle to the image bounds
if (auto cel = obj->cel(L)) {
Tx tx(cel->sprite());
tx(new cmd::ClearRect(cel, rc.offset(cel->position()), color));
tx.commit();
}
// If the destination image is not related to a sprite, we just draw
// the source image without undo information.
else
doc::fill_rect(img, rc, color); // Clips the rectangle to the image bounds
return 0;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -247,6 +247,13 @@ int Layer_get_tileset(lua_State* L)
return 1;
}
int Layer_get_uuid(lua_State* L)
{
auto* layer = get_docobj<Layer>(L, 1);
push_obj(L, layer->uuid());
return 1;
}
int Layer_set_name(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
@ -446,6 +453,7 @@ const Property Layer_properties[] = {
{ "data", UserData_get_text<Layer>, UserData_set_text<Layer> },
{ "properties", UserData_get_properties<Layer>, UserData_set_properties<Layer> },
{ "tileset", Layer_get_tileset, Layer_set_tileset },
{ "uuid", Layer_get_uuid, nullptr },
{ nullptr, nullptr, nullptr }
};

View File

@ -40,6 +40,8 @@ int secure_io_lines(lua_State* L);
int secure_io_input(lua_State* L);
int secure_io_output(lua_State* L);
int secure_os_execute(lua_State* L);
int secure_os_remove(lua_State* L);
int secure_os_rename(lua_State* L);
int secure_package_loadlib(lua_State* L);
enum {
@ -49,6 +51,8 @@ enum {
io_input,
io_output,
os_execute,
os_remove,
os_rename,
package_loadlib,
};
@ -64,6 +68,8 @@ static struct {
{ "io", "input", secure_io_input },
{ "io", "output", secure_io_output },
{ "os", "execute", secure_os_execute },
{ "os", "remove", secure_os_remove },
{ "os", "rename", secure_os_rename },
{ "package", "loadlib", secure_package_loadlib },
};
@ -185,6 +191,81 @@ int secure_os_execute(lua_State* L)
return replaced_functions[os_execute].origfunc(L);
}
int file_result(lua_State* L, bool result, int errorNo = 0, const std::string& fileName = "")
{
if (result) {
lua_pushboolean(L, 1);
return 1;
}
luaL_pushfail(L);
if (fileName.empty())
lua_pushstring(L, strerror(errorNo));
else
lua_pushfstring(L, "%s: %s", fileName.c_str(), strerror(errorNo));
lua_pushinteger(L, errorNo);
return 3;
}
int secure_os_remove(lua_State* L)
{
const std::string absFilename = base::get_canonical_path(luaL_checkstring(L, 1));
if (absFilename.empty())
return file_result(L, false, ENOENT, absFilename);
if (!ask_access(L, absFilename.data(), FileAccessMode::Write, ResourceType::File))
return file_result(L, false, EACCES, absFilename);
if (base::is_directory(absFilename)) {
try {
base::remove_directory(absFilename);
return file_result(L, true);
}
catch (const std::exception&) {
return file_result(L, false, EIO, absFilename);
}
}
try {
base::delete_file(absFilename);
}
catch (const std::exception&) {
return file_result(L, false, EIO, absFilename);
}
return file_result(L, true);
}
int secure_os_rename(lua_State* L)
{
const std::string absSourceFilename = base::get_canonical_path(luaL_checkstring(L, 1));
const std::string absDestFilename = base::get_absolute_path(luaL_checkstring(L, 2));
lua_pop(L, 2);
if (absSourceFilename.empty())
return file_result(L, false, ENOENT, absSourceFilename);
if (absDestFilename.empty())
return file_result(L, false, EINVAL, absDestFilename);
if (!ask_access(L, absSourceFilename.data(), FileAccessMode::Write, ResourceType::File))
return file_result(L, false, EACCES, absSourceFilename);
try {
// If the destination file already exists, we should ask for permission to overwrite it.
if (!base::get_canonical_path(absDestFilename).empty() &&
!ask_access(L, absDestFilename.data(), FileAccessMode::Write, ResourceType::File)) {
return file_result(L, false, EACCES, absDestFilename);
}
base::move_file(absSourceFilename, absDestFilename);
return file_result(L, true);
}
catch (const std::exception&) {
return file_result(L, false, EIO, absSourceFilename);
}
}
int secure_package_loadlib(lua_State* L)
{
const char* cmd = luaL_checkstring(L, 1);
@ -201,7 +282,7 @@ void overwrite_unsecure_functions(lua_State* L)
{
// Remove unsupported functions
lua_getglobal(L, "os");
for (const char* name : { "remove", "rename", "exit", "tmpname" }) {
for (const char* name : { "exit", "tmpname" }) {
lua_pushcfunction(L, unsupported);
lua_setfield(L, -2, name);
}
@ -280,7 +361,15 @@ bool ask_access(lua_State* L,
{
std::string label;
switch (resourceType) {
case ResourceType::File: label = Strings::script_access_file_label(); break;
case ResourceType::File: {
if (mode == FileAccessMode::Write) {
label = Strings::script_access_file_write_label();
}
else {
label = Strings::script_access_file_label();
}
break;
}
case ResourceType::Command: label = Strings::script_access_command_label(); break;
case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break;
case ResourceType::Clipboard: label = Strings::script_access_clipboard_label(); break;

View File

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

View File

@ -64,6 +64,7 @@
#include "doc/tag.h"
#include "doc/tileset.h"
#include "doc/tilesets.h"
#include "undo/undo_state.h"
#include <algorithm>
@ -1012,6 +1013,59 @@ int Sprite_set_tileManagementPlugin(lua_State* L)
return 0;
}
int Sprite_get_useLayerUuids(lua_State* L)
{
auto* sprite = get_docobj<Sprite>(L, 1);
lua_pushboolean(L, sprite->useLayerUuids());
return 1;
}
int Sprite_set_useLayerUuids(lua_State* L)
{
auto* sprite = get_docobj<Sprite>(L, 1);
if (lua_isboolean(L, 2)) {
const bool value = lua_toboolean(L, 2);
sprite->useLayerUuids(value);
}
return 0;
}
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[] = {
{ "__eq", Sprite_eq },
{ "resize", Sprite_resize },
@ -1076,6 +1130,8 @@ const Property Sprite_properties[] = {
{ "pixelRatio", Sprite_get_pixelRatio, Sprite_set_pixelRatio },
{ "events", Sprite_get_events, nullptr },
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
{ "undoHistory", Sprite_get_undoHistory, nullptr },
{ nullptr, nullptr, nullptr }
};

View File

@ -21,9 +21,17 @@
namespace app { namespace thumb {
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel, const gfx::Size& fitInSize)
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
const bool scaleUpToFit,
const gfx::Size& fitInSize)
{
gfx::Size newSize(gfx::Rect(cel->bounds()).fitIn(gfx::Rect(fitInSize)).size());
gfx::Size newSize;
if (scaleUpToFit || cel->bounds().w > fitInSize.w || cel->bounds().h > fitInSize.h)
newSize = gfx::Rect(cel->bounds()).fitIn(gfx::Rect(fitInSize)).size();
else
newSize = cel->bounds().size();
if (newSize.w < 1 || newSize.h < 1)
return nullptr;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2016 Carlo Caputo
//
// This program is distributed under the terms of
@ -22,7 +22,9 @@ class Surface;
namespace app { namespace thumb {
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel, const gfx::Size& fitInSize);
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
const bool scaleUpToFit,
const gfx::Size& fitInSize);
}} // namespace app::thumb

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2021-2024 Igara Studio S.A.
// Copyright (C) 2021-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -20,7 +20,7 @@ void Symmetry::generateStrokes(const Stroke& stroke, Strokes& strokes, ToolLoop*
{
Stroke stroke2;
strokes.push_back(stroke);
gen::SymmetryMode symmetryMode = loop->getSymmetry()->mode();
const gen::SymmetryMode symmetryMode = tools::Symmetry::resolveMode(loop->getSymmetry()->mode());
switch (symmetryMode) {
case gen::SymmetryMode::NONE: ASSERT(false); break;
@ -171,4 +171,14 @@ void Symmetry::calculateSymmetricalStroke(const Stroke& refStroke,
}
}
gen::SymmetryMode Symmetry::resolveMode(gen::SymmetryMode mode)
{
return (((int(mode) & int(gen::SymmetryMode::HORIZONTAL)) ||
(int(mode) & int(gen::SymmetryMode::VERTICAL))) &&
((int(mode) & int(gen::SymmetryMode::RIGHT_DIAG)) ||
(int(mode) & int(gen::SymmetryMode::LEFT_DIAG)))) ?
gen::SymmetryMode::ALL :
mode;
}
}} // namespace app::tools

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2021-2024 Igara Studio S.A.
// Copyright (C) 2021-2025 Igara Studio S.A.
// Copyright (C) 2015 David Capello
//
// This program is distributed under the terms of
@ -38,6 +38,8 @@ public:
gen::SymmetryMode mode() const { return m_symmetryMode; }
static gen::SymmetryMode resolveMode(gen::SymmetryMode mode);
private:
void calculateSymmetricalStroke(const Stroke& refStroke,
Stroke& stroke,

View File

@ -19,10 +19,10 @@
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui_context.h"
#include "os/menus.h"
#include "ui/accelerator.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/scale.h"
#include "ui/shortcut.h"
#include "ui/size_hint_event.h"
#include "ui/widget.h"
@ -139,8 +139,8 @@ void AppMenuItem::onSizeHint(SizeHintEvent& ev)
border().width();
size.h = textHeight() + border().height();
if (m_key && !m_key->accels().empty()) {
size.w += font()->textLength(m_key->accels().front().toString());
if (m_key && !m_key->shortcuts().empty()) {
size.w += font()->textLength(m_key->shortcuts().front().toString());
}
}

View File

@ -431,8 +431,8 @@ void BrushPopup::regenerate(ui::Display* display, const gfx::Point& pos)
params.set("change", "custom");
params.set("slot", base::convert_to<std::string>(slot).c_str());
KeyPtr key = KeyboardShortcuts::instance()->command(CommandId::ChangeBrush(), params);
if (key && !key->accels().empty())
shortcut = key->accels().front().toString();
if (key && !key->shortcuts().empty())
shortcut = key->shortcuts().front().toString();
}
m_customBrushes->addItem(new SelectBrushItem(brush, slot));
m_customBrushes->addItem(new BrushShortcutItem(shortcut, slot));

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -69,10 +69,13 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
m_box->thumbEnabled()->Click.connect([this] { onThumbEnabledChange(); });
m_box->thumbOverlayEnabled()->Click.connect([this] { onThumbOverlayEnabledChange(); });
m_box->thumbOverlaySize()->Change.connect([this] { onThumbOverlaySizeChange(); });
m_box->thumbScaleUpToFit()->Click.connect([this] { onScaleUpToFitChange(); });
const bool visibleThumb = docPref().thumbnails.enabled();
m_box->thumbHSeparator()->setVisible(visibleThumb);
m_box->thumbBox()->setVisible(visibleThumb);
m_box->defaults()->Click.connect([this] { onSetAsDefaults(); });
}
Doc* ConfigureTimelinePopup::doc()
@ -128,6 +131,7 @@ void ConfigureTimelinePopup::updateWidgetsFromCurrentSettings()
m_box->thumbBox()->setVisible(visibleThumb);
m_box->thumbOverlayEnabled()->setSelected(docPref.thumbnails.overlayEnabled());
m_box->thumbOverlaySize()->setValue(docPref.thumbnails.overlaySize());
m_box->thumbScaleUpToFit()->setSelected(docPref.thumbnails.scaleUpToFit());
expandWindow(sizeHint());
}
@ -237,4 +241,33 @@ void ConfigureTimelinePopup::onThumbOverlaySizeChange()
docPref().thumbnails.overlaySize(m_box->thumbOverlaySize()->getValue());
}
void ConfigureTimelinePopup::onScaleUpToFitChange()
{
docPref().thumbnails.scaleUpToFit(m_box->thumbScaleUpToFit()->isSelected());
}
void ConfigureTimelinePopup::onSetAsDefaults()
{
const auto& docPref = this->docPref();
auto& defaults = Preferences::instance().document(nullptr);
defaults.timeline.firstFrame(docPref.timeline.firstFrame());
defaults.thumbnails.enabled(docPref.thumbnails.enabled());
defaults.thumbnails.zoom(docPref.thumbnails.zoom());
defaults.thumbnails.overlayEnabled(docPref.thumbnails.overlayEnabled());
defaults.thumbnails.overlaySize(docPref.thumbnails.overlaySize());
defaults.thumbnails.scaleUpToFit(docPref.thumbnails.scaleUpToFit());
defaults.onionskin.active(docPref.onionskin.active());
defaults.onionskin.prevFrames(docPref.onionskin.prevFrames());
defaults.onionskin.nextFrames(docPref.onionskin.nextFrames());
defaults.onionskin.opacityBase(docPref.onionskin.opacityBase());
defaults.onionskin.opacityStep(docPref.onionskin.opacityStep());
defaults.onionskin.type(docPref.onionskin.type());
defaults.onionskin.loopTag(docPref.onionskin.loopTag());
defaults.onionskin.currentLayer(docPref.onionskin.currentLayer());
defaults.onionskin.position(docPref.onionskin.position());
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -46,6 +47,8 @@ protected:
void onThumbEnabledChange();
void onThumbOverlayEnabledChange();
void onThumbOverlaySizeChange();
void onScaleUpToFitChange();
void onSetAsDefaults();
private:
void updateWidgetsFromCurrentSettings();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -1530,14 +1530,10 @@ public:
DocumentPreferences& docPref = Preferences::instance().document(doc);
at(0)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::HORIZONTAL) ? true : false);
at(1)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::VERTICAL) ? true : false);
at(2)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::RIGHT_DIAG) ? true : false);
at(3)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::LEFT_DIAG) ? true : false);
at(0)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::HORIZONTAL));
at(1)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::VERTICAL));
at(2)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::RIGHT_DIAG));
at(3)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::LEFT_DIAG));
}
private:
@ -1551,60 +1547,7 @@ private:
DocumentPreferences& docPref = Preferences::instance().document(doc);
auto oldMode = docPref.symmetry.mode();
int mode = 0;
if (at(0)->isSelected())
mode |= int(app::gen::SymmetryMode::HORIZONTAL);
if (at(1)->isSelected())
mode |= int(app::gen::SymmetryMode::VERTICAL);
if (at(2)->isSelected())
mode |= int(app::gen::SymmetryMode::RIGHT_DIAG);
if (at(3)->isSelected())
mode |= int(app::gen::SymmetryMode::LEFT_DIAG);
// Non sense symmetries filter:
// - H + 1Diag
// - V + 1Diag
// - H + V + 1Diag
const bool HorV = (mode & int(app::gen::SymmetryMode::HORIZONTAL)) ||
(mode & int(app::gen::SymmetryMode::VERTICAL));
const bool HxorV = !(mode & int(app::gen::SymmetryMode::HORIZONTAL)) !=
!(mode & int(app::gen::SymmetryMode::VERTICAL));
const bool RDxorLD = !(mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) !=
!(mode & int(app::gen::SymmetryMode::LEFT_DIAG));
if (oldMode == gen::SymmetryMode::HORIZONTAL || oldMode == gen::SymmetryMode::VERTICAL ||
oldMode == gen::SymmetryMode::BOTH) {
if (HorV && RDxorLD) {
mode = int(app::gen::SymmetryMode::ALL);
at(0)->setSelected(true);
at(1)->setSelected(true);
at(2)->setSelected(true);
at(3)->setSelected(true);
}
}
else if (oldMode == gen::SymmetryMode::ALL) {
if (HxorV) {
mode = int(app::gen::SymmetryMode::BOTH_DIAG);
at(0)->setSelected(false);
at(1)->setSelected(false);
}
else if (RDxorLD) {
mode = int(app::gen::SymmetryMode::BOTH);
at(2)->setSelected(false);
at(3)->setSelected(false);
}
}
else if ((oldMode == gen::SymmetryMode::RIGHT_DIAG || oldMode == gen::SymmetryMode::LEFT_DIAG ||
oldMode == gen::SymmetryMode::BOTH_DIAG) &&
HorV) {
mode = int(app::gen::SymmetryMode::ALL);
at(0)->setSelected(true);
at(1)->setSelected(true);
at(2)->setSelected(true);
at(3)->setSelected(true);
}
// Non sense symmetries filter end
if (at(0)->isSelected())
mode |= int(app::gen::SymmetryMode::HORIZONTAL);
if (at(1)->isSelected())
@ -1914,7 +1857,7 @@ private:
class ContextBar::FontSelector : public FontEntry {
public:
FontSelector(ContextBar* contextBar)
FontSelector(ContextBar* contextBar) : FontEntry(true) // With stroke and fill options
{
// Load the font from the preferences
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
@ -2616,6 +2559,11 @@ FontInfo ContextBar::fontInfo() const
return m_fontSelector->info();
}
FontEntry* ContextBar::fontEntry()
{
return m_fontSelector;
}
render::DitheringMatrix ContextBar::ditheringMatrix()
{
return m_ditheringSelector->ditheringMatrix();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -90,6 +90,7 @@ public:
// For text tool
FontInfo fontInfo() const;
FontEntry* fontEntry();
// For gradients
render::DitheringMatrix ditheringMatrix();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -41,10 +41,10 @@
#include "doc/layer.h"
#include "doc/sprite.h"
#include "fmt/format.h"
#include "ui/accelerator.h"
#include "ui/alert.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/shortcut.h"
#include "ui/system.h"
#include "ui/view.h"
@ -147,11 +147,11 @@ protected:
KeyPtr rmb = keys->action(KeyAction::RightMouseButton, KeyContext::Any);
// Convert action keys into mouse messages.
if (lmb->isPressed(msg, *keys) || rmb->isPressed(msg, *keys)) {
if (lmb->isPressed(msg) || rmb->isPressed(msg)) {
MouseMessage mouseMsg(
(msg->type() == kKeyDownMessage ? kMouseDownMessage : kMouseUpMessage),
PointerType::Unknown,
(lmb->isPressed(msg, *keys) ? kButtonLeft : kButtonRight),
(lmb->isPressed(msg) ? kButtonLeft : kButtonRight),
msg->modifiers(),
mousePosInDisplay());

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -457,6 +457,8 @@ void BrushPreview::show(const gfx::Point& screenPos)
// Here we re-use the cached surface
if (!cached && m_uiLayer->surface()) {
m_uiLayer->surface()->clear();
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2022-2024 Igara Studio S.A.
// Copyright (C) 2022-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -25,10 +25,10 @@ DelayedMouseMove::DelayedMouseMove(DelayedMouseMoveDelegate* delegate,
: m_delegate(delegate)
, m_editor(editor)
, m_timer(interval)
, m_spritePos(std::numeric_limits<float>::min(), std::numeric_limits<float>::min())
, m_mouseMoveReceived(false)
, m_mouseDownPos(kNoPosReceived)
, m_mouseDownTime(base::current_tick())
, m_spritePos(std::numeric_limits<float>::min(), std::numeric_limits<float>::min())
{
ASSERT(m_delegate);
m_timer.Tick.connect([this] { commitMouseMove(); });

View File

@ -31,6 +31,7 @@
#include "app/tools/active_tool.h"
#include "app/tools/controller.h"
#include "app/tools/ink.h"
#include "app/tools/symmetry.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
#include "app/ui/color_bar.h"
@ -57,6 +58,7 @@
#include "app/util/tile_flags_utils.h"
#include "base/chrono.h"
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "doc/doc.h"
#include "doc/mask_boundaries.h"
#include "doc/slice.h"
@ -265,6 +267,23 @@ void Editor::setStateInternal(const EditorStatePtr& newState)
{
m_brushPreview.hide();
// Some onLeaveState impls (like the ones from MovingPixelsState,
// WritingTextState, MovingSelectionState) might generate a
// Tx/Transaction::commit(), which will add a new undo state,
// triggering a sprite change scripting event
// (SpriteEvents::onAddUndoState). This event could be handled by an
// extension and that extension might want to save the current
// sprite (e.g. calling Sprite_saveCopyAs, the kind of extension
// that takes snapshots after each sprite change). That will be a
// new Context::executeCommand() for the save command, generating a
// BeforeCommandExecution signal, getting back to onLeaveState
// again. In that case, we just ignore the reentry as the first
// onLeaveState should handle everything (to avoid an stack
// overflow/infinite recursion).
if (m_leavingState)
return;
base::ScopedValue leaving(m_leavingState, true);
// Fire before change state event, set the state, and fire after
// change state event.
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());
@ -836,6 +855,86 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g,
m_docPref.grid.forceSection();
}
m_docPref.show.grid.forceDirtyFlag();
// Symmetry mode
if (isActive() && (m_flags & Editor::kShowSymmetryLine) &&
Preferences::instance().symmetryMode.enabled()) {
const int symmetryButtons = int(m_docPref.symmetry.mode());
// Symmetry::resolveMode is to calculate the right symmetry
// mode. This is necessary because some symmetry settings
// do not make sense and should be forced to 'ALL'
const int mode = int(tools::Symmetry::resolveMode(m_docPref.symmetry.mode()));
const gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
const gfx::Color semiTransparentColor =
gfx::rgba(rgba_getr(color), rgba_getg(color), rgba_getb(color), rgba_geta(color) / 4);
const double x = int(m_proj.applyX<double>(m_docPref.symmetry.xAxis()));
const double y = int(m_proj.applyY<double>(m_docPref.symmetry.yAxis()));
if (mode & int(app::gen::SymmetryMode::HORIZONTAL) && x > 0) {
g->drawVLine(symmetryButtons & int(app::gen::SymmetryMode::HORIZONTAL) ?
color :
semiTransparentColor,
enclosingRect.x + x,
enclosingRect.y,
enclosingRect.h);
}
if (mode & int(app::gen::SymmetryMode::VERTICAL) && y > 0) {
g->drawHLine(
symmetryButtons & int(app::gen::SymmetryMode::VERTICAL) ? color : semiTransparentColor,
enclosingRect.x,
enclosingRect.y + y,
enclosingRect.w);
}
if (mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) {
// Bottom point intersection:
gfx::Point bottomLeft(
enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(y - enclosingRect.h),
enclosingRect.y2());
if (bottomLeft.x < enclosingRect.x) {
// Left intersection
bottomLeft.y = enclosingRect.y2() +
m_proj.turnXinTermsOfY<int>(bottomLeft.x - enclosingRect.x);
bottomLeft.x = enclosingRect.x;
}
// Top intersection
gfx::Point topRight(enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(y),
enclosingRect.y);
if (enclosingRect.x2() < topRight.x) {
// Right intersection
topRight.y = enclosingRect.y +
m_proj.applyY<int>(m_proj.removeX<int>(topRight.x - enclosingRect.x2()));
topRight.x = enclosingRect.x2();
}
g->drawLine(symmetryButtons & int(app::gen::SymmetryMode::RIGHT_DIAG) ?
color :
semiTransparentColor,
bottomLeft,
topRight);
}
if (mode & int(app::gen::SymmetryMode::LEFT_DIAG)) {
// Bottom point intersection:
gfx::Point bottomRight(
enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(enclosingRect.h - y),
enclosingRect.y2());
if (enclosingRect.x2() < bottomRight.x) {
// Left intersection
bottomRight.y = enclosingRect.y2() +
m_proj.turnXinTermsOfY<int>(enclosingRect.x2() - bottomRight.x);
bottomRight.x = enclosingRect.x2();
}
// Top intersection
gfx::Point topLeft(enclosingRect.x + x - m_proj.turnYinTermsOfX<int>(y), enclosingRect.y);
if (topLeft.x < enclosingRect.x) {
// Right intersection
topLeft.y = enclosingRect.y + m_proj.turnXinTermsOfY<int>(enclosingRect.x - topLeft.x);
topLeft.x = enclosingRect.x;
}
g->drawLine(
symmetryButtons & int(app::gen::SymmetryMode::LEFT_DIAG) ? color : semiTransparentColor,
topLeft,
bottomRight);
}
}
}
}
}
@ -908,86 +1007,6 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
enclosingRect = gfx::Rect(spriteRect.x, spriteRect.y, spriteRect.w * 3, spriteRect.h * 3);
}
// Symmetry mode
if (isActive() && (m_flags & Editor::kShowSymmetryLine) &&
Preferences::instance().symmetryMode.enabled()) {
int mode = int(m_docPref.symmetry.mode());
if (mode & int(app::gen::SymmetryMode::HORIZONTAL)) {
double x = m_docPref.symmetry.xAxis();
if (x > 0) {
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
g->drawVLine(
color,
spriteRect.x + m_proj.applyX(mainTilePosition().x) + int(m_proj.applyX<double>(x)),
enclosingRect.y,
enclosingRect.h);
}
}
if (mode & int(app::gen::SymmetryMode::VERTICAL)) {
double y = m_docPref.symmetry.yAxis();
if (y > 0) {
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
g->drawHLine(
color,
enclosingRect.x,
spriteRect.y + m_proj.applyY(mainTilePosition().y) + int(m_proj.applyY<double>(y)),
enclosingRect.w);
}
}
if (mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) {
double y = m_docPref.symmetry.yAxis();
double x = m_docPref.symmetry.xAxis();
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
// Bottom point intersection:
gfx::Point bottomLeft(
enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x)) -
(enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyY<double>(y))),
enclosingRect.y2());
if (bottomLeft.x < enclosingRect.x) {
// Left intersection
bottomLeft.y = enclosingRect.y2() - enclosingRect.x + bottomLeft.x;
bottomLeft.x = enclosingRect.x;
}
// Top intersection
gfx::Point topRight(enclosingRect.x + m_proj.applyY(mainTilePosition().x) +
int(m_proj.applyX<double>(x)) + m_proj.applyY(mainTilePosition().y) +
int(m_proj.applyY<double>(y)),
enclosingRect.y);
if (enclosingRect.x2() < topRight.x) {
// Right intersection
topRight.y = enclosingRect.y + topRight.x - enclosingRect.x2();
topRight.x = enclosingRect.x2();
}
g->drawLine(color, bottomLeft, topRight);
}
if (mode & int(app::gen::SymmetryMode::LEFT_DIAG)) {
double y = m_docPref.symmetry.yAxis();
double x = m_docPref.symmetry.xAxis();
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
// Bottom point intersection:
gfx::Point bottomRight(
enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x)) +
(enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyX<double>(y))),
enclosingRect.y2());
if (enclosingRect.x2() < bottomRight.x) {
// Left intersection
bottomRight.y = enclosingRect.y2() - bottomRight.x + enclosingRect.x2();
bottomRight.x = enclosingRect.x2();
}
// Top intersection
gfx::Point topLeft(enclosingRect.x + m_proj.applyY(mainTilePosition().x) +
int(m_proj.applyX<double>(x)) - m_proj.applyY(mainTilePosition().y) -
int(m_proj.applyY<double>(y)),
enclosingRect.y);
if (topLeft.x < enclosingRect.x) {
// Right intersection
topLeft.y = enclosingRect.y + enclosingRect.x - topLeft.x;
topLeft.x = enclosingRect.x;
}
g->drawLine(color, topLeft, bottomRight);
}
}
// Draw active layer/cel edges
if ((m_docPref.show.layerEdges() || m_showAutoCelGuides) &&
// Show layer edges and possibly cel guides only on states that
@ -2452,8 +2471,10 @@ void Editor::onTiledModeChange()
screenPos = editorToScreen(spritePos);
auto lastPoint = document()->lastDrawingPoint();
lastPoint += mainTilePosition() - m_oldMainTilePos;
document()->setLastDrawingPoint(lastPoint);
if (lastPoint != Doc::NoLastDrawingPoint()) {
lastPoint += mainTilePosition() - m_oldMainTilePos;
document()->setLastDrawingPoint(lastPoint);
}
centerInSpritePoint(spritePos);
}
@ -2536,8 +2557,6 @@ void Editor::onBeforeLayerEditableChange(DocEvent& ev, bool newState)
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
{
Rect vp = View::getView(this)->viewportBounds();
bool used = false;
if (m_sprite)
used = m_state->onSetCursor(this, mouseDisplayPos);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -458,6 +458,13 @@ private:
DocView* m_docView;
// Special flag to avoid re-entering a new state when we are leaving
// the current one. This avoids an infinite onLeaveState() recursion
// in some special cases when an extension (third-party code)
// creates a new sprite change in the same sprite change scripting
// event.
bool m_leavingState = false;
// Last known mouse position received by this editor when the
// mouse button was pressed. Used for auto-scrolling. To get the
// current mouse position on the editor you can use

View File

@ -428,8 +428,8 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
if (editor->slicesTransforms())
drawExtraCel();
// Redraw the editor.
editor->invalidate();
// Notify changes
m_site.document()->notifyGeneralUpdate();
// Use StandbyState implementation
return StandbyState::onMouseMove(editor, msg);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -420,10 +420,12 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
// Now we calculate the difference from x1,y1 point and we can
// use it to adjust all coordinates (x1, y1, x2, y2).
bounds.setOrigin(gridOffset);
newTransformation.pivot(abs_initial_pivot - m_initialData.bounds().origin() + gridOffset);
}
else
newTransformation.pivot(abs_initial_pivot + gfx::PointF(dx, dy));
newTransformation.bounds(bounds);
newTransformation.pivot(abs_initial_pivot + gfx::PointF(dx, dy));
break;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -464,8 +464,7 @@ bool StandbyState::onKeyDown(Editor* editor, KeyMessage* msg)
checkStartDrawingStraightLine(editor, nullptr, nullptr))
return false;
Keys keys = KeyboardShortcuts::instance()->getDragActionsFromKeyMessage(KeyContext::MouseWheel,
msg);
Keys keys = KeyboardShortcuts::instance()->getDragActionsFromKeyMessage(msg);
if (editor->hasMouse() && !keys.empty()) {
// Don't enter DraggingValueState to change brush size if we are
// in a selection-like tool

View File

@ -15,6 +15,7 @@
#include "app/commands/command.h"
#include "app/extra_cel.h"
#include "app/fonts/font_info.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/site.h"
#include "app/tx.h"
@ -43,12 +44,42 @@
#include "os/skia/skia_surface.h"
#endif
#include <cmath>
namespace app {
using namespace ui;
// Get ui::Paint to render text from context bar options / preferences
static ui::Paint get_paint_for_text()
{
ui::Paint paint;
if (auto* app = App::instance()) {
if (auto* ctxBar = app->contextBar())
paint = ctxBar->fontEntry()->paint();
}
paint.color(color_utils::color_for_ui(Preferences::instance().colorBar.fgColor()));
return paint;
}
static gfx::RectF calc_blob_bounds(const text::TextBlobRef& blob)
{
gfx::RectF bounds = get_text_blob_required_bounds(blob);
ui::Paint paint = get_paint_for_text();
if (paint.style() == ui::Paint::Style::Stroke ||
paint.style() == ui::Paint::Style::StrokeAndFill) {
bounds.enlarge(std::ceil(paint.strokeWidth()));
}
return bounds;
}
class WritingTextState::TextEditor : public Entry {
public:
enum TextPreview {
Intermediate, // With selection preview / user interface
Final, // Final to be rendered in the cel
};
TextEditor(Editor* editor, const Site& site, const gfx::Rect& bounds)
: Entry(4096, "")
, m_editor(editor)
@ -61,7 +92,7 @@ public:
setPersistSelection(true);
createExtraCel(site, bounds);
renderExtraCelBase();
renderExtraCel(TextPreview::Intermediate);
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
@ -76,35 +107,37 @@ public:
// Returns the extra cel with the text rendered (but without the
// selected text highlighted).
ExtraCelRef extraCel()
ExtraCelRef extraCel(const TextPreview textPreview)
{
renderExtraCelBase();
renderExtraCelText(false);
renderExtraCel(textPreview);
return m_extraCel;
}
void setExtraCelBounds(const gfx::Rect& bounds)
void setExtraCelBounds(const gfx::RectF& bounds)
{
if (bounds.w != m_extraCel->image()->width() || bounds.h != m_extraCel->image()->height()) {
doc::Image* extraImg = m_extraCel->image();
if (!extraImg || std::ceil(bounds.w) != extraImg->width() ||
std::ceil(bounds.h) != extraImg->height()) {
createExtraCel(m_editor->getSite(), bounds);
}
else {
m_baseBounds = bounds;
m_extraCel->cel()->setBounds(bounds);
}
renderExtraCelBase();
renderExtraCelText(true);
renderExtraCel(TextPreview::Intermediate);
}
obs::signal<void(const gfx::Size&)> NewRequiredBounds;
obs::signal<void(const gfx::RectF&)> NewRequiredBounds;
private:
void createExtraCel(const Site& site, const gfx::Rect& bounds)
{
m_baseBounds = bounds;
m_extraCel->create(ExtraCel::Purpose::TextPreview,
site.tilemapMode(),
site.sprite(),
bounds,
bounds.size(),
gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h)),
site.frame(),
255);
@ -175,7 +208,7 @@ private:
// Notify that we could make the text editor bigger to show this
// text blob.
NewRequiredBounds(get_text_blob_required_size(blob));
NewRequiredBounds(calc_blob_bounds(blob));
}
void onPaint(PaintEvent& ev) override
@ -204,8 +237,7 @@ private:
}
// Render extra cel with text + selected text
renderExtraCelBase();
renderExtraCelText(true);
renderExtraCel(TextPreview::Intermediate);
m_doc->setExtraCel(m_extraCel);
// Paint caret
@ -226,62 +258,80 @@ private:
}
}
void renderExtraCelBase()
void renderExtraCel(const TextPreview textPreview)
{
auto extraImg = m_extraCel->image();
extraImg->clear(extraImg->maskColor());
render::Render().renderLayer(extraImg,
m_editor->layer(),
m_editor->frame(),
gfx::Clip(0, 0, m_extraCel->cel()->bounds()),
doc::BlendMode::SRC);
}
doc::Image* extraImg = m_extraCel->image();
ASSERT(extraImg);
if (!extraImg)
return;
void renderExtraCelText(const bool withSelection)
{
const auto textColor = color_utils::color_for_image(Preferences::instance().colorBar.fgColor(),
IMAGE_RGB);
extraImg->clear(extraImg->maskColor());
text::TextBlobRef blob = textBlob();
if (!blob)
return;
doc::ImageRef blobImage;
gfx::RectF bounds;
if (blob) {
const ui::Paint paint = get_paint_for_text();
bounds = calc_blob_bounds(blob);
blobImage = render_text_blob(blob, bounds, get_paint_for_text());
if (!blobImage)
return;
doc::ImageRef image = render_text_blob(blob, textColor);
if (!image)
return;
// Invert selected range in the image
if (textPreview == TextPreview::Intermediate) {
Range range;
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
if (!range.isEmpty()) {
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
// Invert selected range in the image
if (withSelection) {
Range range;
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
if (!range.isEmpty()) {
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
if (!selectedBounds.isEmpty()) {
selectedBounds.offset(-bounds.origin());
if (!selectedBounds.isEmpty()) {
#ifdef LAF_SKIA
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(blobImage.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
os::Paint paint;
paint.blendMode(os::BlendMode::Xor);
paint.color(textColor);
surface->drawRect(selectedBounds, paint);
os::Paint paint2 = paint;
paint2.blendMode(os::BlendMode::Xor);
paint2.style(os::Paint::Style::Fill);
surface->drawRect(selectedBounds, paint2);
#endif // LAF_SKIA
}
}
}
}
doc::blend_image(m_extraCel->image(),
image.get(),
gfx::Clip(image->bounds().size()),
m_doc->sprite()->palette(m_editor->frame()),
255,
doc::BlendMode::NORMAL);
doc::Cel* extraCel = m_extraCel->cel();
ASSERT(extraCel);
if (!extraCel)
return;
extraCel->setPosition(m_baseBounds.x + bounds.x, m_baseBounds.y + bounds.y);
render::Render().renderLayer(extraImg,
m_editor->layer(),
m_editor->frame(),
gfx::Clip(0, 0, extraCel->bounds()),
doc::BlendMode::SRC);
if (blobImage) {
doc::blend_image(extraImg,
blobImage.get(),
gfx::Clip(blobImage->bounds().size()),
m_doc->sprite()->palette(m_editor->frame()),
255,
doc::BlendMode::NORMAL);
}
}
Editor* m_editor;
Doc* m_doc;
ExtraCelRef m_extraCel;
// Initial bounds for the entry field. This can be modified later to
// render the text in case some initial letter/glyph needs some
// extra room at the left side.
gfx::Rect m_baseBounds;
};
WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
@ -297,10 +347,10 @@ WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
m_fontChangeConn =
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
m_entry->NewRequiredBounds.connect([this](const gfx::Size& blobSize) {
if (m_bounds.w < blobSize.w || m_bounds.h < blobSize.h) {
m_bounds.w = std::max(m_bounds.w, blobSize.w);
m_bounds.h = std::max(m_bounds.h, blobSize.h);
m_entry->NewRequiredBounds.connect([this](const gfx::RectF& blobBounds) {
if (m_bounds.w < blobBounds.w || m_bounds.h < blobBounds.h) {
m_bounds.w = std::max(m_bounds.w, blobBounds.w);
m_bounds.h = std::max(m_bounds.h, blobBounds.h);
m_entry->setExtraCelBounds(m_bounds);
m_entry->setBounds(calcEntryBounds());
}
@ -373,11 +423,11 @@ void WritingTextState::onCommitMouseMove(Editor* editor, const gfx::PointF& spri
if (!m_movingBounds)
return;
gfx::Point delta(spritePos - m_cursorStart);
gfx::PointF delta(spritePos - m_cursorStart);
if (delta.x == 0 && delta.y == 0)
return;
m_bounds.setOrigin(gfx::Point(delta + m_boundsOrigin));
m_bounds.setOrigin(delta + m_boundsOrigin);
m_entry->setExtraCelBounds(m_bounds);
m_entry->setBounds(calcEntryBounds());
}
@ -393,12 +443,7 @@ bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
return true;
}
bool WritingTextState::onKeyDown(Editor*, KeyMessage*)
{
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
bool WritingTextState::onKeyDown(Editor*, KeyMessage* msg)
{
// Cancel loop pressing Esc key
if (msg->scancode() == ui::kKeyEsc) {
@ -407,7 +452,17 @@ bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
// Drop text pressing Enter key
else if (msg->scancode() == ui::kKeyEnter) {
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;
}
@ -460,8 +515,8 @@ EditorState::LeaveAction WritingTextState::onLeaveState(Editor* editor, EditorSt
// Paints the text in the active layer/sprite creating an
// undoable transaction.
Site site = m_editor->getSite();
ExtraCelRef extraCel = m_entry->extraCel();
Tx tx(site.document(), "Text Tool");
ExtraCelRef extraCel = m_entry->extraCel(TextEditor::Final);
Tx tx(site.document(), Strings::tools_text());
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
expand.validateDestCanvas(gfx::Region(extraCel->cel()->bounds()));
@ -512,7 +567,7 @@ void WritingTextState::onFontChange(const FontInfo& fontInfo, FontEntry::From fr
// This is useful to show changes to the anti-alias option
// immediately.
auto dummy = m_entry->extraCel();
auto dummy = m_entry->extraCel(TextEditor::Intermediate);
if (fromField == FontEntry::From::Popup) {
if (m_entry)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
// Copyright (c) 2022-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -59,7 +59,7 @@ private:
DelayedMouseMove m_delayedMouseMove;
Editor* m_editor;
gfx::Rect m_bounds;
gfx::RectF m_bounds;
std::unique_ptr<TextEditor> m_entry;
// True if the text was discarded.
@ -71,7 +71,7 @@ private:
bool m_mouseMoveReceived = false;
bool m_movingBounds = false;
gfx::PointF m_cursorStart;
gfx::Point m_boundsOrigin;
gfx::PointF m_boundsOrigin;
obs::scoped_connection m_beforeCmdConn;
obs::scoped_connection m_fontChangeConn;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -46,7 +46,7 @@ ExportFileWindow::ExportFileWindow(const Doc* doc)
base = base::join_path(basePath, base::get_file_title(base));
std::string newFn = base::replace_extension(base, defaultExtension());
if (newFn == base) {
if (newFn == base::replace_extension(base, base::get_file_extension(doc->filename()))) {
newFn = base::join_path(
base::get_file_path(newFn),
base::get_file_title(newFn) + "-export." + base::get_file_extension(newFn));

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -18,6 +18,7 @@
#include "base/time.h"
#include "os/surface.h"
#include "text/font.h"
#include "text/font_metrics.h"
#include "ui/ui.h"
#include <algorithm>
@ -44,6 +45,7 @@ FileList::FileList()
, m_multiselect(false)
, m_zoom(1.0)
, m_itemsPerRow(0)
, m_showHidden(Preferences::instance().fileSelector.showHidden())
{
setFocusStop(true);
setDoubleBuffered(true);
@ -172,6 +174,14 @@ void FileList::animateToZoom(const double zoom)
startAnimation(ANI_ZOOM, 10);
}
void FileList::setShowHidden(const bool show)
{
m_showHidden = show;
m_req_valid = false;
m_selected = nullptr;
regenerateList();
}
bool FileList::onProcessMessage(Message* msg)
{
switch (msg->type()) {
@ -401,7 +411,6 @@ void FileList::onPaint(ui::PaintEvent& ev)
gfx::Rect bounds = clientBounds();
g->fillRect(theme->colors.background(), bounds);
// g->fillRect(bgcolor, gfx::Rect(bounds.x, y, bounds.w, itemSize.h));
int i = 0, selectedIndex = -1;
for (IFileItem* fi : m_list) {
@ -506,17 +515,28 @@ void FileList::paintItem(ui::Graphics* g, IFileItem* fi, const int i)
// item name
if (isIconView() && textBounds.w > info.bounds.w) {
g->drawAlignedUIText(fi->displayName().c_str(),
g->drawAlignedUIText(fi->displayName(),
fgcolor,
bgcolor,
(textBounds & gfx::Rect(info.bounds).shrink(2 * guiscale())),
ui::CENTER | ui::TOP | ui::CHARWRAP);
}
else {
g->drawText(fi->displayName().c_str(),
fgcolor,
bgcolor,
gfx::Point(textBounds.x + 2 * guiscale(), textBounds.y + 2 * guiscale()));
auto blob = text::TextBlob::MakeWithShaper(theme->fontMgr(), font(), fi->displayName());
if (blob) {
Paint paint;
paint.color(fgcolor);
paint.style(os::Paint::Fill);
text::FontMetrics metrics;
font()->metrics(&metrics);
const float baselineDelta = -metrics.ascent - blob->baseline();
g->drawTextBlob(
blob,
gfx::PointF(textBounds.x + 2 * guiscale(), textBounds.y + 2 * guiscale() + baselineDelta),
paint);
}
}
// Draw thumbnail progress bar
@ -814,7 +834,7 @@ void FileList::regenerateList()
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
IFileItem* fileitem = *it;
if (fileitem->isHidden())
if (fileitem->isHidden() && !m_showHidden)
it = m_list.erase(it);
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
it = m_list.erase(it);

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