Compare commits

...

89 Commits

Author SHA1 Message Date
Martín Capello 935283e78c Internationalize " Copy" string usage 2025-08-27 09:25:04 -03:00
Martín Capello 357c054432 Use "Copy" suffix to name duplicated slices 2025-08-27 09:25:04 -03:00
Martín Capello e0f33a0413 Offset duplicated slices to avoid overlapping 2025-08-27 09:25:04 -03:00
Martín Capello 50ede4f062 Add slices copy&paste and duplication (fix #4466) 2025-08-27 09:25:04 -03:00
David Capello 5f7cc42333 Remove unnecessary call to regenerateCols() in Timeline::refresh()
Related to:
https://github.com/aseprite/aseprite/pull/5367#issuecomment-3225287017
2025-08-26 15:39:55 -03:00
Christian Kaiser 8e75cfc4c7 [lua] Refresh timeline when changing layer collapsed status (fix #5366) 2025-08-26 15:35:28 -03:00
David Capello 983b07383f [lua] Fix default autofit for a Dialog() when autofit is not specified (#5176 / #5321) 2025-08-26 11:23:48 -03:00
Christian Kaiser c57554646b Escape characters in the console that we can't show properly (fix #5324) 2025-08-26 11:21:08 -03:00
Christian Kaiser 74953174d6 [lua] Added `autofit` and `sizeHint` properties to Dialog (fix #5176) 2025-08-26 11:05:06 -03:00
Christian Kaiser 49fa35237a Activate the native window when asking the user to save sprite changes (fix #3542 / #5318) 2025-08-26 09:57:51 -03:00
Christian Kaiser 0ccf9dcc4f [lua] Add app.tip (#5316 / #5348) 2025-08-26 09:00:40 -03:00
David Capello 194f8424a8 Add right-click to configure the cancel selection button (#5102 / #5145) 2025-08-26 07:55:14 -03:00
Joshua Lopez debab653fa Fix marquee tool escape deselection (fix #5102) 2025-08-25 16:51:03 -03:00
hwabis 6e9024d54d Fix mouse wheel zooming not working with zoom tool (quick) 2025-08-25 16:35:14 -03:00
David Capello 1fa7fd0831 Minor UI fixes for dialogs with user data
This patch unifies the behavior of all dialogs with user data to
expand the window vertically onToggleUserData().
2025-08-25 16:20:37 -03:00
David Capello ab6b040e83 Rename Mask::m_freeze_count -> Mask::m_freezes 2025-08-25 15:30:23 -03:00
Gaspar Capello bc312a37b3 Fix Color Management regression (fix #5333)
Before this fix, the assigned color space was always sRGB (default),
so this color space was used for the rest of the rendering.
Now the color space for the back layer of the display
has been made explicit.
2025-08-25 13:10:25 -03:00
Christian Kaiser 3129fda977 [lua] Add missing "DIAGONAL" FlipType (fix #5359) 2025-08-17 14:58:26 -03:00
David Capello 6cb61fb41e Remove deprecated issue template 2025-08-14 14:17:56 -03:00
David Capello aa817a8d2a
Update issue templates 2025-08-14 14:11:47 -03:00
David Capello 40031f83d8 [lua] Fix typo in Sprite:newCel() error 2025-08-13 13:06:05 -03:00
David Capello 90282dbc40 Fix cast error in HarfBuzz library between function types
This includes the following patch:
60c6b7786d
2025-08-11 15:29:58 -03:00
Martín Capello 1227f9c49c Refactor getLayerIndexFromSprite into LayerGroup 2025-08-11 14:47:56 -03:00
Martín Capello d61ae919ad Fix crash dropping file on timeline (fix #5289) 2025-08-11 14:47:56 -03:00
David Capello b2b2583176 [lua] Update scripting API version to 35 2025-08-07 20:18:10 -03:00
David Capello b535212642 Update laf module 2025-08-07 20:14:43 -03:00
Christian Kaiser 229a3cdf65 [lua] Add onchecked parameter to Commands 2025-08-07 18:57:28 -03:00
Christian Kaiser b3814ec912 Unify ContextBar updates when moving pixels, tooltips (fix #5329) 2025-08-06 20:20:42 -03:00
David Capello e88f3bb413 Show error if curl/unzip tools aren't available (fix #5309) 2025-08-06 14:44:21 -03:00
Christian Kaiser eaa2bdf0af [lua] Process mnemonics consistently for plugins (fix #5250) 2025-08-04 15:58:29 -03:00
Christian Kaiser 57309e5aa5 Allow gif encoding to be stopped (fix #2619) 2025-08-01 20:46:32 -03:00
Christian Kaiser 4bb9239f50 [lua] Add `resizeable` property to Dialog constructor (fix #5177)
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-08-01 18:57:50 -03:00
David Capello cef92c1a38 Add .plist files for macOS
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
We don't have an Aseprite.app target in cmake files yet, but we might
add it in a near future.
2025-07-28 16:18:19 -03:00
Christian Kaiser 22e72ab5cb [win] Fix includeDesktopDir returning the default path
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
Uses SHGFP_TYPE_CURRENT which returns the Desktop that the user has configured instead of the default, fixes Windows 11's OneDrive Desktop folder.
2025-07-28 10:47:53 -03:00
Christian Kaiser 80fa065bd5 [lua] Add sprite.undoHistory
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-25 13:58:52 -03:00
David Capello de1ccb24dd [win] Don't drop text when IME dialog composition is accepted w/Enter
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
With #5230, now that we can show the IME dialog on Windows, when we
are selecting a specific word/composition in the IME dialog, if we
press Enter we'll receive that Enter onKeyUp(). It's better if we
process the Enter key onKeyDown() (as the IME enter key is not
received in that case).
2025-07-25 09:19:50 -03:00
David Capello 7d91c4b9d9 [win] Fix dead keys on Windows 2025-07-24 17:45:46 -03:00
Cerallin 6d89a6bc15 fix entry
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-24 12:50:32 -03:00
Cerallin d4e97b5a96 Add const method Entry::caretPosOnScreen()
This method is for Entry::setTextInput() and IME positioning.
2025-07-24 12:50:32 -03:00
Cerallin 205b18dc0f Make Entry::getCharBoxBounds() a const method 2025-07-24 12:50:32 -03:00
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
138 changed files with 2569 additions and 937 deletions

View File

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

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

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

View File

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

View File

@ -4,7 +4,7 @@ Aseprite is being developed and maintained currently by [Igara Studio](https://i
The active team of developers is:
* [David Capello](https://github.com/dacap)
* [Gaspar Capello](https://github.com/Gsparoken)
* [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)

View File

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

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

@ -1203,6 +1203,7 @@
<menu id="slice_popup_menu">
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
<item command="DuplicateSlice" text="@.duplicate" group="slice_popup_duplicate" />
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
</menu>

View File

@ -146,6 +146,10 @@
<value id="KEEP_AS_IS" value="1" />
<value id="RAW_IMAGE" value="2" />
</enum>
<enum id="CancelSelection">
<value id="DISCARD" value="0" />
<value id="DESELECT" value="1" />
</enum>
</types>
<global>
@ -325,6 +329,7 @@
<option id="force_rotsprite" type="bool" default="false" />
<option id="multicel_when_layers_or_frames" type="bool" default="true" />
<option id="snap_to_grid" type="bool" default="true" />
<option id="cancel_selection" type="CancelSelection" default="CancelSelection::DISCARD" />
</section>
<section id="quantization">
<option id="with_alpha" type="bool" default="true" />
@ -356,6 +361,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" />

View File

@ -265,6 +265,7 @@ Despeckle = Despeckle
DeveloperConsole = Developer Console
DiscardBrush = Discard Brush
DuplicateLayer = Duplicate Layer
DuplicateSlice = Duplicate Slice
DuplicateSprite = Duplicate Sprite
DuplicateView = Duplicate View
Exit = Exit
@ -554,6 +555,8 @@ amount = Amount:
flatten = Merge layers
[context_bar]
discard_changes = Discard Changes
deselect = Deselect
center = Center
fit_screen = Fit Screen
back = Back
@ -582,7 +585,7 @@ pixel_perfect = Pixel-perfect
linear_gradient = Linear Gradient
radial_gradient = Radial Gradient
drop_pixel = Drop pixels here (Enter)
cancel_drag = Cancel drag and drop (Esc)
cancel_drag = Cancel drag and drop (Esc)\nRight-click: Configure action
auto_select_layer = Auto Select Layer
all = All
none = None
@ -621,6 +624,14 @@ current_layer = Current Layer
first_ref_layer = First Reference Layer
pick = Pick:
sample = Sample:
position_label = P:
rotation_label = R:
position_x = X Position
position_y = Y Position
size_width = Width
size_height = Height
rotation_angle = Angle
rotation_skew = Skew
[convolution_matrix]
reload_stock = &Reload Stock
@ -765,6 +776,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
@ -816,6 +828,7 @@ same_in_all_tools = Same in all Tools
opacity = Opacity:
tolerance = Tolerance:
show_more = Show more...
copy_of = {} Copy
[general_text]
copy = &Copy
@ -1663,6 +1676,10 @@ from = From:
to = To:
tolerance = Tolerance:
[duplicate_slice]
x_duplicated = Slice "{}" duplicated
n_slices_duplicated = {} slice(s) duplicated
[remove_slice]
x_removed = Slice "{}" removed
n_slices_removed = {} slice(s) removed
@ -1707,7 +1724,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
@ -1747,6 +1764,7 @@ delete_file = Delete file, I've already sent it
[slice_popup_menu]
properties = Slice &Properties...
duplicate = D&uplicate Slice
delete = &Delete Slice
[slice_properties]
@ -1834,6 +1852,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
@ -1896,6 +1926,7 @@ timeline_show = Show Timeline
[undo_history]
title = Undo History
initial_state = Initial State
[user_data]
user_data = User Data:

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

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

View File

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

2
laf

@ -1 +1 @@
Subproject commit 7d30a582e5c8655fab368c4d61f3125855a7b30d
Subproject commit 39f6dca9c623e2d005a5c5609afa97ac749b4e33

View File

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

@ -393,6 +393,7 @@ target_sources(app-lib PRIVATE
commands/cmd_deselect_mask.cpp
commands/cmd_discard_brush.cpp
commands/cmd_duplicate_layer.cpp
commands/cmd_duplicate_slice.cpp
commands/cmd_duplicate_sprite.cpp
commands/cmd_duplicate_view.cpp
commands/cmd_enter_license.cpp
@ -669,7 +670,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
@ -715,6 +716,7 @@ target_sources(app-lib PRIVATE
util/render_text.cpp
util/resize_image.cpp
util/shader_helpers.cpp
util/slice_utils.cpp
util/tile_flags_utils.cpp
util/tileset_utils.cpp
util/wrap_point.cpp

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

@ -1046,7 +1046,11 @@ public:
}
protected:
void onOpen(Event& evt) override { sectionListbox()->selectIndex(m_curSection); }
void onOpen(Event& evt) override
{
sectionListbox()->selectIndex(m_curSection);
app::gen::Options::onOpen(evt);
}
private:
void onInitTheme(InitThemeEvent& ev) override
@ -1368,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

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

View File

@ -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) 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
@ -40,6 +40,7 @@ FOR_EACH_COMMAND(DeselectMask)
FOR_EACH_COMMAND(Despeckle)
FOR_EACH_COMMAND(DiscardBrush)
FOR_EACH_COMMAND(DuplicateLayer)
FOR_EACH_COMMAND(DuplicateSlice)
FOR_EACH_COMMAND(DuplicateSprite)
FOR_EACH_COMMAND(DuplicateView)
FOR_EACH_COMMAND(Exit)

View File

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

View File

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

View File

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

View File

@ -70,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();
@ -81,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;
}
@ -323,7 +424,7 @@ public:
dithering,
sprite ? sprite->rgbMap(0) : nullptr,
sprite ? sprite->palette(0) : nullptr,
sprite ? sprite->backgroundLayer() : nullptr,
(sprite && sprite->backgroundLayer()),
0,
0,
nullptr);

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 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
@ -1095,6 +1095,9 @@ public:
gifframe_t nframes = totalFrames();
for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) {
ASSERT(frame_it != frame_end);
if (m_fop->isStop())
break;
frame_t frame = *frame_it;
++frame_it;
@ -1401,7 +1404,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,7 +14,9 @@
#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>
@ -87,9 +89,9 @@ text::FontRef FontData::getFont(text::FontMgrRef& fontMgr, float size)
// 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
}

View File

@ -34,7 +34,8 @@ public:
void setName(const std::string& name) { m_name = name; }
void setFilename(const std::string& filename) { m_filename = filename; }
void setDefaultSize(const float size) { m_size = size; }
void setAntialias(bool antialias) { m_antialias = antialias; }
void 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;

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 33
#define API_VERSION 35
#endif

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -221,7 +221,7 @@ int secure_os_remove(lua_State* L)
base::remove_directory(absFilename);
return file_result(L, true);
}
catch (std::exception& e) {
catch (const std::exception&) {
return file_result(L, false, EIO, absFilename);
}
}
@ -229,7 +229,7 @@ int secure_os_remove(lua_State* L)
try {
base::delete_file(absFilename);
}
catch (std::exception& e) {
catch (const std::exception&) {
return file_result(L, false, EIO, absFilename);
}
@ -261,7 +261,7 @@ int secure_os_rename(lua_State* L)
base::move_file(absSourceFilename, absDestFilename);
return file_result(L, true);
}
catch (std::exception& e) {
catch (const std::exception&) {
return file_result(L, false, EIO, absSourceFilename);
}
}

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>
@ -456,7 +457,7 @@ int Sprite_newCel(lua_State* L)
auto sprite = get_docobj<Sprite>(L, 1);
auto layerBase = get_docobj<Layer>(L, 2);
if (!layerBase->isImage())
return luaL_error(L, "unexpected kinf of layer in Sprite:newCel()");
return luaL_error(L, "unexpected kind of layer in Sprite:newCel()");
frame_t frame = get_frame_number_from_arg(L, 3);
if (frame < 0 || frame > sprite->lastFrame())
@ -1029,6 +1030,42 @@ int Sprite_set_useLayerUuids(lua_State* L)
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 },
@ -1094,6 +1131,7 @@ const Property Sprite_properties[] = {
{ "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

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -388,6 +388,7 @@ FOR_ENUM(app::gen::TimelinePosition)
FOR_ENUM(app::gen::ToGrayAlgorithm)
FOR_ENUM(app::gen::WindowColorProfile)
FOR_ENUM(app::gen::AlphaRange)
FOR_ENUM(app::gen::CancelSelection)
FOR_ENUM(app::tools::ColorFromTo)
FOR_ENUM(app::tools::DynamicSensor)
FOR_ENUM(app::tools::FreehandAlgorithm)

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

@ -1001,12 +1001,12 @@ public:
m_angle.setSuffix("°");
m_skew.setSuffix("°");
addChild(new Label("P:"));
addChild(new Label(Strings::context_bar_position_label()));
addChild(&m_x);
addChild(&m_y);
addChild(&m_w);
addChild(&m_h);
addChild(new Label("R:"));
addChild(new Label(Strings::context_bar_rotation_label()));
addChild(&m_angle);
addChild(&m_skew);
@ -1047,6 +1047,16 @@ public:
m_skew.Change.connect([this] { onChangeSkew(); });
}
void setupTooltips(TooltipManager* tooltipManager)
{
tooltipManager->addTooltipFor(&m_x, Strings::context_bar_position_x(), BOTTOM);
tooltipManager->addTooltipFor(&m_y, Strings::context_bar_position_y(), BOTTOM);
tooltipManager->addTooltipFor(&m_w, Strings::context_bar_size_width(), BOTTOM);
tooltipManager->addTooltipFor(&m_h, Strings::context_bar_size_height(), BOTTOM);
tooltipManager->addTooltipFor(&m_angle, Strings::context_bar_rotation_angle(), BOTTOM);
tooltipManager->addTooltipFor(&m_skew, Strings::context_bar_rotation_skew(), BOTTOM);
}
void update(const Transformation& t)
{
auto rc = t.bounds();
@ -1397,6 +1407,7 @@ public:
}
obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
obs::signal<void(ContextBarObserver::DropAction, const gfx::Point&)> ConfigureDropPixels;
protected:
void onItemChange(Item* item) override
@ -1408,6 +1419,21 @@ protected:
case 1: DropPixels(ContextBarObserver::CancelDrag); break;
}
}
void onRightClick(Item* item) override
{
ButtonSet::onRightClick(item);
const gfx::Rect rc = item->bounds();
const gfx::Point pt(rc.x, rc.y2());
auto action = ContextBarObserver::DropPixels;
switch (selectedItem()) {
case 0: action = ContextBarObserver::DropPixels; break;
case 1: action = ContextBarObserver::CancelDrag; break;
}
ConfigureDropPixels(action, pt);
}
};
class ContextBar::EyedropperField : public HBox {
@ -1857,7 +1883,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);
@ -1942,6 +1968,8 @@ ContextBar::ContextBar(TooltipManager* tooltipManager, ColorBar* colorBar)
m_keysConn = KeyboardShortcuts::instance()->UserChange.connect(
[this, tooltipManager] { setupTooltips(tooltipManager); });
m_dropPixelsConn = m_dropPixels->DropPixels.connect(&ContextBar::onDropPixels, this);
m_configureDropPixelsConn =
m_dropPixels->ConfigureDropPixels.connect(&ContextBar::onConfigureDropPixels, this);
setActiveBrush(createBrushFromPreferences());
@ -2086,6 +2114,14 @@ void ContextBar::onDropPixels(ContextBarObserver::DropAction action)
notify_observers(&ContextBarObserver::onDropPixels, action);
}
void ContextBar::onConfigureDropPixels(ContextBarObserver::DropAction action, const gfx::Point& pt)
{
notify_observers<ContextBarObserver::DropAction, const gfx::Point&>(
&ContextBarObserver::onConfigureDropPixels,
action,
pt);
}
void ContextBar::updateSliceFields(const Site& site)
{
if (site.sprite())
@ -2559,6 +2595,11 @@ FontInfo ContextBar::fontInfo() const
return m_fontSelector->info();
}
FontEntry* ContextBar::fontEntry()
{
return m_fontSelector;
}
render::DitheringMatrix ContextBar::ditheringMatrix()
{
return m_ditheringSelector->ditheringMatrix();
@ -2621,6 +2662,7 @@ void ContextBar::setupTooltips(TooltipManager* tooltipManager)
m_dropPixels->setupTooltips(tooltipManager);
m_symmetry->setupTooltips(tooltipManager);
m_sliceFields->setupTooltips(tooltipManager);
m_transformation->setupTooltips(tooltipManager);
}
void ContextBar::registerCommands()

View File

@ -1,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();
@ -128,6 +129,7 @@ private:
void onFgOrBgColorChange(doc::Brush::ImageColor imageColor);
void onOpacityRangeChange();
void onDropPixels(ContextBarObserver::DropAction action);
void onConfigureDropPixels(ContextBarObserver::DropAction action, const gfx::Point& pt);
void updateSliceFields(const Site& site);
// ActiveToolObserver impl
@ -212,6 +214,7 @@ private:
obs::scoped_connection m_alphaRangeConn;
obs::scoped_connection m_keysConn;
obs::scoped_connection m_dropPixelsConn;
obs::scoped_connection m_configureDropPixelsConn;
obs::scoped_connection m_sizeConn;
obs::scoped_connection m_angleConn;
obs::scoped_connection m_opacityConn;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -8,6 +9,8 @@
#define APP_CONTEXT_BAR_OBSERVER_H_INCLUDED
#pragma once
#include "gfx/fwd.h"
namespace app {
class ContextBarObserver {
@ -16,6 +19,7 @@ public:
virtual ~ContextBarObserver() {}
virtual void onDropPixels(DropAction action) {}
virtual void onConfigureDropPixels(DropAction action, const gfx::Point& pt) {}
};
} // namespace app

View File

@ -180,6 +180,7 @@ DitheringSelector::DitheringSelector(Type type) : m_type(type)
m_extChanges = extensions.DitheringMatricesChange.connect([this] { regenerate(); });
setUseCustomWidget(true);
regenerate();
}
void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
@ -189,16 +190,6 @@ void DitheringSelector::onInitTheme(ui::InitThemeEvent& ev)
setSizeHint(calcItemSizeHint(0));
}
void DitheringSelector::onVisible(bool visible)
{
if (visible && !m_initialized) {
// Only do the expensive regeneration when we're set as visible for the first time.
regenerate();
m_initialized = true;
}
ComboBox::onVisible(visible);
}
void DitheringSelector::setSelectedItemByName(const std::string& name)
{
int index = findItemIndex(name);

View File

@ -31,7 +31,6 @@ public:
protected:
void onInitTheme(ui::InitThemeEvent& ev) override;
void onVisible(bool visible) override;
private:
void regenerate(int selectedItemIndex = 0);
@ -39,7 +38,6 @@ private:
Type m_type;
obs::scoped_connection m_extChanges;
bool m_initialized = false;
};
} // namespace app

View File

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

View File

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

@ -58,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"
@ -266,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());
@ -2537,6 +2555,16 @@ void Editor::onBeforeLayerEditableChange(DocEvent& ev, bool newState)
m_state->onBeforeLayerEditableChange(this, ev.layer(), newState);
}
void Editor::onBeforeSlicesDuplication(DocEvent& ev)
{
clearSlicesSelection();
}
void Editor::onSliceDuplicated(DocEvent& ev)
{
selectSlice(ev.slice());
}
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
{
bool used = false;

View File

@ -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
@ -343,6 +343,8 @@ protected:
void onRemoveSlice(DocEvent& ev) override;
void onBeforeLayerVisibilityChange(DocEvent& ev, bool newState) override;
void onBeforeLayerEditableChange(DocEvent& ev, bool newState) override;
void onBeforeSlicesDuplication(DocEvent& ev) override;
void onSliceDuplicated(DocEvent& ev) override;
// ActiveToolObserver impl
void onActiveToolChange(tools::Tool* tool) override;
@ -458,6 +460,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

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -22,6 +22,7 @@
#include "app/commands/commands.h"
#include "app/commands/move_thing.h"
#include "app/console.h"
#include "app/i18n/strings.h"
#include "app/modules/gui.h"
#include "app/pref/preferences.h"
#include "app/tools/ink.h"
@ -48,6 +49,7 @@
#include "fmt/format.h"
#include "gfx/rect.h"
#include "ui/manager.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/system.h"
#include "ui/view.h"
@ -286,9 +288,6 @@ bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
UIContext* ctx = UIContext::instance();
ctx->setActiveView(editor->getDocView());
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(getTransformation(editor));
// Start scroll loop
if (editor->checkForScroll(msg) || editor->checkForZoom(msg))
return true;
@ -442,10 +441,6 @@ void MovingPixelsState::onCommitMouseMove(Editor* editor, const gfx::PointF& spr
// Drag the image to that position
m_pixelsMovement->moveImage(spritePos, moveModifier);
// Update context bar and status bar
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(transformation);
m_editor->updateStatusBar();
}
@ -475,16 +470,13 @@ bool MovingPixelsState::onKeyDown(Editor* editor, KeyMessage* msg)
// FineControl now (e.g. if we pressed another modifier key).
m_lockedKeyAction = KeyAction::None;
if (msg->scancode() == kKeyEnter || // TODO make this key customizable
msg->scancode() == kKeyEnterPad || msg->scancode() == kKeyEsc) {
// TODO make these keys customizable
if (msg->scancode() == kKeyEsc) {
cancelDrag();
return true;
}
if (msg->scancode() == kKeyEnter || msg->scancode() == kKeyEnterPad) {
dropPixels();
// The escape key drop pixels and deselect the mask.
if (msg->scancode() == kKeyEsc) { // TODO make this key customizable
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
}
return true;
}
@ -529,6 +521,10 @@ bool MovingPixelsState::onUpdateStatusBar(Editor* editor)
const Transformation& transform(getTransformation(editor));
gfx::Size imageSize = m_pixelsMovement->getInitialImageSize();
// Update the context bar along with the status bar
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(transform);
int w = int(transform.bounds().w);
int h = int(transform.bounds().h);
int gcd = base::gcd(w, h);
@ -796,14 +792,64 @@ void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action)
switch (action) {
case ContextBarObserver::DropPixels: dropPixels(); break;
case ContextBarObserver::CancelDrag: cancelDrag(); break;
}
}
case ContextBarObserver::CancelDrag:
void MovingPixelsState::cancelDrag()
{
if (!m_pixelsMovement || m_discarded)
return;
switch (Preferences::instance().selection.cancelSelection()) {
case gen::CancelSelection::DISCARD:
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
m_discarded = true;
// Quit from MovingPixelsState, back to standby.
m_editor->backToPreviousState();
dropPixels();
break;
case gen::CancelSelection::DESELECT: {
dropPixels();
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
break;
}
}
}
void MovingPixelsState::onConfigureDropPixels(ContextBarObserver::DropAction action,
const gfx::Point& pt)
{
if (!isActiveEditor())
return;
switch (action) {
case ContextBarObserver::DropPixels:
// Do nothing
break;
case ContextBarObserver::CancelDrag: {
Menu menu;
MenuItem discardChanges(Strings::context_bar_discard_changes());
MenuItem deselect(Strings::context_bar_deselect());
menu.addChild(&discardChanges);
menu.addChild(&deselect);
auto& opt = Preferences::instance().selection.cancelSelection;
discardChanges.setSelected(opt() == gen::CancelSelection::DISCARD);
deselect.setSelected(opt() == gen::CancelSelection::DESELECT);
discardChanges.Click.connect([&opt] { opt(gen::CancelSelection::DISCARD); });
deselect.Click.connect([&opt] { opt(gen::CancelSelection::DESELECT); });
ContextBar* contextBar = App::instance()->contextBar();
menu.showPopup(pt, contextBar->display());
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-2017 David Capello
//
// This program is distributed under the terms of
@ -77,6 +77,7 @@ public:
// ContextBarObserver
void onDropPixels(ContextBarObserver::DropAction action) override;
void onConfigureDropPixels(ContextBarObserver::DropAction action, const gfx::Point& pt) override;
// PixelsMovementDelegate
void onPivotChange() override;
@ -95,6 +96,7 @@ private:
void setTransparentColor(bool opaque, const app::Color& color);
void dropPixels();
void cancelDrag();
bool isActiveDocument() const;
bool isActiveEditor() const;

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

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

View File

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

View File

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

View File

@ -54,6 +54,7 @@ public:
double zoom() const { return m_zoom; }
void setZoom(const double zoom);
void animateToZoom(const double zoom);
void setShowHidden(const bool show);
obs::signal<void()> FileSelected;
obs::signal<void()> FileAccepted;
@ -137,6 +138,7 @@ private:
double m_toZoom;
int m_itemsPerRow;
bool m_showHidden;
};
} // namespace app

View File

@ -316,6 +316,7 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
for (auto child : viewType()->children())
child->setFocusStop(false);
showHiddenCheck()->setSelected(Preferences::instance().fileSelector.showHidden());
m_fileList = new FileList();
m_fileList->setId("fileview");
m_fileName->setAssociatedFileList(m_fileList);
@ -334,6 +335,10 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
viewType()->ItemChange.connect([this] { onChangeViewType(); });
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
fileType()->Change.connect([this] { onFileTypeChange(); });
showHiddenCheck()->Click.connect([this] {
Preferences::instance().fileSelector.showHidden(showHiddenCheck()->isSelected());
m_fileList->setShowHidden(showHiddenCheck()->isSelected());
});
m_fileList->FileSelected.connect([this] { onFileListFileSelected(); });
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -10,8 +10,10 @@
#include "app/ui/filename_field.h"
#include "app/app.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/recent_files.h"
#include "app/ui/skin/skin_theme.h"
#include "base/fs.h"
#include "ui/box.h"
@ -24,6 +26,11 @@ namespace app {
using namespace ui;
FilenameField::FilenameButton::FilenameButton(const std::string& text) : ButtonSet(1)
{
addItem(text);
}
FilenameField::FilenameField(const Type type, const std::string& pathAndFilename)
: m_entry(type == EntryAndButton ? new ui::Entry(1024, "") : nullptr)
, m_button(type == EntryAndButton ? Strings::select_file_browse() : Strings::select_file_text())
@ -46,7 +53,10 @@ FilenameField::FilenameField(const Type type, const std::string& pathAndFilename
if (m_entry)
m_entry->Change.connect([this] { setFilename(updatedFilename()); });
m_button.Click.connect([this] { onBrowse(); });
m_button.ItemChange.connect([this](ButtonSet::Item* item) {
m_button.setSelectedItem(nullptr);
onBrowse();
});
initTheme();
m_editFullPathChangeConn = Preferences::instance().general.editFullPath.AfterChange.connect(
@ -94,7 +104,6 @@ void FilenameField::onSetEditFullPath()
void FilenameField::onBrowse()
{
const gfx::Rect bounds = m_button.bounds();
m_button.setSelected(false);
ui::Menu menu;
ui::MenuItem choose(Strings::select_file_choose());
@ -107,6 +116,11 @@ void FilenameField::onBrowse()
menu.addChild(&relative);
menu.addChild(&absolute);
if (auto* recent = App::instance()->recentFiles()) {
addFoldersToMenu(&menu, recent->pinnedFolders(), Strings::file_selector_pinned_folders());
addFoldersToMenu(&menu, recent->recentFolders(), Strings::file_selector_recent_folders());
}
choose.Click.connect([this] {
std::string fn = SelectOutputFile();
if (!fn.empty()) {
@ -120,6 +134,21 @@ void FilenameField::onBrowse()
menu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
}
void FilenameField::addFoldersToMenu(ui::Menu* menu,
const base::paths& folders,
const std::string& separatorTitle)
{
if (folders.empty())
return;
menu->addChild(new ui::Separator(separatorTitle, ui::HORIZONTAL));
for (const std::string& folder : folders) {
MenuItem* folderItem = new MenuItem(folder);
folderItem->Click.connect([this, folder] { setFilename(base::join_path(folder, m_file)); });
menu->addChild(folderItem);
}
}
void FilenameField::setFilename(const std::string& pathAndFilename)
{
const std::string spritePath = base::get_file_path(m_docFilename);
@ -164,11 +193,6 @@ void FilenameField::onInitTheme(ui::InitThemeEvent& ev)
{
HBox::onInitTheme(ev);
setChildSpacing(0);
auto theme = skin::SkinTheme::get(this);
ui::Style* style = theme->styles.miniButton();
if (style)
m_button.setStyle(style);
}
void FilenameField::onUpdateText()
@ -181,9 +205,9 @@ void FilenameField::updateWidgets()
if (m_entry)
m_entry->setText(displayedFilename());
else if (m_file.empty())
m_button.setText(Strings::select_file_text());
m_button.getItem(0)->setText(Strings::select_file_text());
else
m_button.setText(displayedFilename());
m_button.getItem(0)->setText(displayedFilename());
Change();
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -8,14 +8,19 @@
#define APP_UI_FILENAME_FIELD_H_INCLUDED
#pragma once
#include "app/ui/button_set.h"
#include "base/paths.h"
#include "obs/connection.h"
#include "obs/signal.h"
#include "ui/box.h"
#include "ui/button.h"
#include "ui/entry.h"
#include <string>
namespace ui {
class Menu;
}
namespace app {
class FilenameField : public ui::HBox {
@ -44,17 +49,25 @@ protected:
void onSetEditFullPath();
private:
class FilenameButton : public ButtonSet {
public:
FilenameButton(const std::string& text);
};
void setEditFullPath(const bool on);
void updateWidgets();
void onBrowse();
std::string updatedFilename() const;
void addFoldersToMenu(ui::Menu* menu,
const base::paths& folders,
const std::string& separatorTitle);
std::string m_path;
std::string m_pathBase;
std::string m_file;
std::string m_docFilename;
ui::Entry* m_entry;
ui::Button m_button;
FilenameButton m_button;
bool m_editFullPath;
bool m_askOverwrite;

View File

@ -12,10 +12,12 @@
#include "app/app.h"
#include "app/console.h"
#include "app/i18n/strings.h"
#include "app/recent_files.h"
#include "app/ui/font_popup.h"
#include "app/ui/skin/skin_theme.h"
#include "base/contains.h"
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "fmt/format.h"
#include "ui/display.h"
@ -166,6 +168,12 @@ void FontEntry::FontFace::onChange()
base::ScopedValue lock(m_fromEntryChange, true);
SearchEntry::onChange();
// This shouldn't happen, but we received crash reports where the
// m_popup is nullptr here.
ASSERT(m_popup);
if (!m_popup)
return;
m_popup->setSearchText(text());
// Changing the search text doesn't generate a FontChange
@ -207,8 +215,13 @@ void FontEntry::FontFace::onCloseIconPressed()
std::sort(pinnedFonts.begin(), pinnedFonts.end());
}
// Refill the list with the new pinned/unpinned item
m_popup->recreatePinnedItems();
// This shouldn't happen, but we received crash reports where the
// m_popup is nullptr here.
ASSERT(m_popup);
if (m_popup) {
// Refill the list with the new pinned/unpinned item
m_popup->recreatePinnedItems();
}
invalidate();
}
@ -251,22 +264,91 @@ void FontEntry::FontSize::onEntryChange()
Change();
}
FontEntry::FontStyle::FontStyle() : ButtonSet(3, true)
FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, true)
{
addItem("B");
addItem("I");
addItem("...");
setMultiMode(MultiMode::Set);
tooltips->addTooltipFor(getItem(0), Strings::text_tool_bold(), BOTTOM);
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
}
FontEntry::FontEntry()
FontEntry::FontStroke::FontStroke(ui::TooltipManager* tooltips) : m_fill(2)
{
auto* theme = skin::SkinTheme::get(this);
m_fill.addItem(theme->parts.toolFilledRectangle(), theme->styles.contextBarButton());
m_fill.addItem(theme->parts.toolRectangle(), theme->styles.contextBarButton());
m_fill.setSelectedItem(0);
m_fill.ItemChange.connect([this] { Change(); });
m_stroke.setText("0");
m_stroke.setSuffix("pt");
m_stroke.ValueChange.connect([this] { Change(); });
addChild(&m_fill);
addChild(&m_stroke);
tooltips->addTooltipFor(m_fill.getItem(0), Strings::shape_fill(), BOTTOM);
tooltips->addTooltipFor(m_fill.getItem(1), Strings::shape_stroke(), BOTTOM);
tooltips->addTooltipFor(&m_stroke, Strings::shape_stroke_width(), BOTTOM);
}
bool FontEntry::FontStroke::fill() const
{
return const_cast<FontStroke*>(this)->m_fill.getItem(0)->isSelected();
}
float FontEntry::FontStroke::stroke() const
{
return m_stroke.textDouble();
}
FontEntry::FontStroke::WidthEntry::WidthEntry() : ui::IntEntry(0, 100, this)
{
}
void FontEntry::FontStroke::WidthEntry::onValueChange()
{
ui::IntEntry::onValueChange();
ValueChange();
}
bool FontEntry::FontStroke::WidthEntry::onAcceptUnicodeChar(int unicodeChar)
{
return (IntEntry::onAcceptUnicodeChar(unicodeChar) || unicodeChar == '.');
}
std::string FontEntry::FontStroke::WidthEntry::onGetTextFromValue(int value)
{
return fmt::format("{:.1f}", value / 10.0);
}
int FontEntry::FontStroke::WidthEntry::onGetValueFromText(const std::string& text)
{
return int(10.0 * base::convert_to<double>(text));
}
FontEntry::FontEntry(const bool withStrokeAndFill)
: m_style(&m_tooltips)
, m_stroke(withStrokeAndFill ? std::make_unique<FontStroke>(&m_tooltips) : nullptr)
{
m_face.setExpansive(true);
m_size.setExpansive(false);
m_style.setExpansive(false);
addChild(&m_tooltips);
addChild(&m_face);
addChild(&m_size);
addChild(&m_style);
if (m_stroke)
addChild(m_stroke.get());
m_tooltips.addTooltipFor(&m_face, Strings::text_tool_font_family(), BOTTOM);
m_tooltips.addTooltipFor(m_size.getEntryWidget(), Strings::text_tool_font_size(), BOTTOM);
m_face.setMinSize(gfx::Size(128 * guiscale(), 0));
@ -288,6 +370,8 @@ FontEntry::FontEntry()
});
m_style.ItemChange.connect(&FontEntry::onStyleItemClick, this);
if (m_stroke)
m_stroke->Change.connect(&FontEntry::onStrokeChange, this);
}
// Defined here as FontPopup type is not fully defined in the header
@ -316,6 +400,29 @@ void FontEntry::setInfo(const FontInfo& info, const From fromField)
FontChange(m_info, fromField);
}
ui::Paint FontEntry::paint()
{
ui::Paint paint;
ui::Paint::Style style = ui::Paint::Fill;
if (m_stroke) {
const float stroke = m_stroke->stroke();
if (m_stroke->fill()) {
if (stroke > 0.0f) {
style = ui::Paint::StrokeAndFill;
paint.strokeWidth(stroke);
}
}
else {
style = ui::Paint::Stroke;
paint.strokeWidth(stroke);
}
}
paint.style(style);
return paint;
}
void FontEntry::onStyleItemClick(ButtonSet::Item* item)
{
text::FontStyle style = m_info.style();
@ -393,4 +500,9 @@ void FontEntry::onStyleItemClick(ButtonSet::Item* item)
}
}
void FontEntry::onStrokeChange()
{
FontChange(m_info, From::Paint);
}
} // namespace app

View File

@ -14,7 +14,11 @@
#include "ui/box.h"
#include "ui/button.h"
#include "ui/combobox.h"
#include "ui/int_entry.h"
#include "ui/paint.h"
#include "ui/tooltips.h"
#include <memory>
#include <string>
namespace app {
@ -30,18 +34,22 @@ public:
Flags,
Hinting,
Popup,
Paint,
};
FontEntry();
FontEntry(bool withStrokeAndFill);
~FontEntry();
FontInfo info() { return m_info; }
void setInfo(const FontInfo& info, From from);
ui::Paint paint();
obs::signal<void(const FontInfo&, From)> FontChange;
private:
void onStyleItemClick(ButtonSet::Item* item);
void onStrokeChange();
class FontFace : public SearchEntry {
public:
@ -73,13 +81,40 @@ private:
class FontStyle : public ButtonSet {
public:
FontStyle();
FontStyle(ui::TooltipManager* tooltips);
};
class FontStroke : public HBox {
public:
FontStroke(ui::TooltipManager* tooltips);
bool fill() const;
float stroke() const;
obs::signal<void()> Change;
private:
class WidthEntry : public ui::IntEntry,
public ui::SliderDelegate {
public:
WidthEntry();
obs::signal<void()> ValueChange;
private:
void onValueChange() override;
bool onAcceptUnicodeChar(int unicodeChar) override;
// SliderDelegate impl
std::string onGetTextFromValue(int value) override;
int onGetValueFromText(const std::string& text) override;
};
ButtonSet m_fill;
WidthEntry m_stroke;
};
ui::TooltipManager m_tooltips;
FontInfo m_info;
FontFace m_face;
FontSize m_size;
FontStyle m_style;
std::unique_ptr<FontStroke> m_stroke;
bool m_lockFace = false;
};

View File

@ -194,7 +194,11 @@ private:
if (!blob)
return;
doc::ImageRef image = render_text_blob(blob, gfx::rgba(0, 0, 0));
ui::Paint paint;
paint.color(gfx::rgba(0, 0, 0));
paint.style(ui::Paint::Fill);
const gfx::RectF textBounds = get_text_blob_required_bounds(blob);
doc::ImageRef image = render_text_blob(blob, textBounds, paint);
if (!image)
return;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -13,7 +13,7 @@
#include "app/ui/key_context.h"
#include "base/convert_to.h"
#include "base/vector2d.h"
#include "ui/accelerator.h"
#include "ui/shortcut.h"
#include <memory>
#include <utility>
@ -106,7 +106,7 @@ inline KeyAction operator&(KeyAction a, KeyAction b)
class Key;
using KeyPtr = std::shared_ptr<Key>;
using Keys = std::vector<KeyPtr>;
using KeySourceAccelList = std::vector<std::pair<KeySource, ui::Accelerator>>;
using KeySourceShortcutList = std::vector<std::pair<KeySource, ui::Shortcut>>;
using DragVector = base::Vector2d<double>;
class Key {
@ -119,29 +119,26 @@ public:
static KeyPtr MakeDragAction(WheelAction dragAction);
KeyType type() const { return m_type; }
const ui::Accelerators& accels() const;
const KeySourceAccelList addsKeys() const { return m_adds; }
const KeySourceAccelList delsKeys() const { return m_dels; }
const ui::Shortcuts& shortcuts() const;
const KeySourceShortcutList& addsKeys() const { return m_adds; }
const KeySourceShortcutList& delsKeys() const { return m_dels; }
void add(const ui::Accelerator& accel, const KeySource source, KeyboardShortcuts& globalKeys);
const ui::Accelerator* isPressed(const ui::Message* msg,
const KeyboardShortcuts& globalKeys,
const KeyContext keyContext) const;
const ui::Accelerator* isPressed(const ui::Message* msg,
const KeyboardShortcuts& globalKeys) const;
void add(const ui::Shortcut& shortcut, KeySource source, KeyboardShortcuts& globalKeys);
const ui::Shortcut* isPressed(const ui::Message* msg, KeyContext keyContext) const;
const ui::Shortcut* isPressed(const ui::Message* msg) const;
bool isPressed() const;
bool isLooselyPressed() const;
bool isCommandListed() const;
bool hasAccel(const ui::Accelerator& accel) const;
bool hasUserDefinedAccels() const;
bool hasShortcut(const ui::Shortcut& shortcut) const;
bool hasUserDefinedShortcuts() const;
// The KeySource indicates from where the key was disabled
// (e.g. if it was removed from an extension-defined file, or from
// user-defined).
void disableAccel(const ui::Accelerator& accel, const KeySource source);
void disableShortcut(const ui::Shortcut& shortcut, KeySource source);
// Resets user accelerators to the original & extension-defined ones.
// Resets user shortcuts to the original & extension-defined ones.
void reset();
void copyOriginalToUser();
@ -164,11 +161,11 @@ public:
private:
KeyType m_type;
KeySourceAccelList m_adds;
KeySourceAccelList m_dels;
// Final list of accelerators after processing the
KeySourceShortcutList m_adds;
KeySourceShortcutList m_dels;
// Final list of shortcuts after processing the
// addition/deletion of extension-defined & user-defined keys.
mutable std::unique_ptr<ui::Accelerators> m_accels;
mutable std::unique_ptr<ui::Shortcuts> m_shortcuts;
KeyContext m_keycontext;
// for KeyType::Command

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -28,8 +28,8 @@
#include "app/xml_document.h"
#include "app/xml_exception.h"
#include "fmt/format.h"
#include "ui/accelerator.h"
#include "ui/message.h"
#include "ui/shortcut.h"
#include "tinyxml2.h"
@ -244,13 +244,13 @@ std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelActio
return std::string();
}
void erase_accel(app::KeySourceAccelList& kvs,
const app::KeySource source,
const ui::Accelerator& accel)
void erase_shortcut(app::KeySourceShortcutList& kvs,
const app::KeySource source,
const ui::Shortcut& shortcut)
{
for (auto it = kvs.begin(); it != kvs.end();) {
auto& kv = *it;
if (kv.first == source && kv.second == accel) {
if (kv.first == source && kv.second == shortcut) {
it = kvs.erase(it);
}
else
@ -258,7 +258,7 @@ void erase_accel(app::KeySourceAccelList& kvs,
}
}
void erase_accels(app::KeySourceAccelList& kvs, const app::KeySource source)
void erase_shortcuts(app::KeySourceShortcutList& kvs, const app::KeySource source)
{
for (auto it = kvs.begin(); it != kvs.end();) {
auto& kv = *it;
@ -436,104 +436,100 @@ KeyPtr Key::MakeDragAction(WheelAction dragAction)
return k;
}
const ui::Accelerators& Key::accels() const
const ui::Shortcuts& Key::shortcuts() const
{
if (!m_accels) {
m_accels = std::make_unique<ui::Accelerators>();
if (!m_shortcuts) {
m_shortcuts = std::make_unique<ui::Shortcuts>();
// Add default keys
for (const auto& kv : m_adds) {
if (kv.first == KeySource::Original)
m_accels->add(kv.second);
m_shortcuts->add(kv.second);
}
// Delete/add extension-defined keys
for (const auto& kv : m_dels) {
if (kv.first == KeySource::ExtensionDefined)
m_accels->remove(kv.second);
m_shortcuts->remove(kv.second);
else {
ASSERT(kv.first != KeySource::Original);
}
}
for (const auto& kv : m_adds) {
if (kv.first == KeySource::ExtensionDefined)
m_accels->add(kv.second);
m_shortcuts->add(kv.second);
}
// Delete/add user-defined keys
for (const auto& kv : m_dels) {
if (kv.first == KeySource::UserDefined)
m_accels->remove(kv.second);
m_shortcuts->remove(kv.second);
}
for (const auto& kv : m_adds) {
if (kv.first == KeySource::UserDefined)
m_accels->add(kv.second);
m_shortcuts->add(kv.second);
}
}
return *m_accels;
return *m_shortcuts;
}
void Key::add(const ui::Accelerator& accel, const KeySource source, KeyboardShortcuts& globalKeys)
void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShortcuts& globalKeys)
{
m_adds.emplace_back(source, accel);
m_accels.reset();
m_adds.emplace_back(source, shortcut);
m_shortcuts.reset();
// Remove the accelerator from other commands
// Remove the shortcut from other commands
if (source == KeySource::ExtensionDefined || source == KeySource::UserDefined) {
erase_accel(m_dels, source, accel);
erase_shortcut(m_dels, source, shortcut);
globalKeys.disableAccel(accel, source, m_keycontext, this);
globalKeys.disableShortcut(shortcut, source, m_keycontext, this);
}
}
const ui::Accelerator* Key::isPressed(const Message* msg,
const KeyboardShortcuts& globalKeys,
const KeyContext keyContext) const
const ui::Shortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
{
if (auto keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
for (const Accelerator& accel : accels()) {
if (accel.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
if (const auto* keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
for (const Shortcut& shortcut : shortcuts()) {
if (shortcut.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
(m_keycontext == KeyContext::Any || m_keycontext == keyContext)) {
return &accel;
return &shortcut;
}
}
}
else if (auto mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
for (const Accelerator& accel : accels()) {
if ((accel.modifiers() == mouseMsg->modifiers()) &&
else if (const auto* mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
for (const Shortcut& shortcut : shortcuts()) {
if ((shortcut.modifiers() == mouseMsg->modifiers()) &&
(m_keycontext == KeyContext::Any ||
// TODO we could have multiple mouse wheel key-context,
// like "sprite editor" context, or "timeline" context,
// etc.
m_keycontext == KeyContext::MouseWheel)) {
return &accel;
return &shortcut;
}
}
}
return nullptr;
}
const ui::Accelerator* Key::isPressed(const Message* msg, const KeyboardShortcuts& globalKeys) const
const ui::Shortcut* Key::isPressed(const Message* msg) const
{
return isPressed(msg, globalKeys, globalKeys.getCurrentKeyContext());
return isPressed(msg, KeyboardShortcuts::getCurrentKeyContext());
}
bool Key::isPressed() const
{
for (const Accelerator& accel : this->accels()) {
if (accel.isPressed())
return true;
}
return false;
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
return shortcut.isPressed();
});
}
bool Key::isLooselyPressed() const
{
for (const Accelerator& accel : this->accels()) {
if (accel.isLooselyPressed())
return true;
}
return false;
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
return shortcut.isLooselyPressed();
});
}
bool Key::isCommandListed() const
@ -541,51 +537,49 @@ bool Key::isCommandListed() const
return type() == KeyType::Command && command()->isListed(params());
}
bool Key::hasAccel(const ui::Accelerator& accel) const
bool Key::hasShortcut(const ui::Shortcut& shortcut) const
{
return accels().has(accel);
return shortcuts().has(shortcut);
}
bool Key::hasUserDefinedAccels() const
bool Key::hasUserDefinedShortcuts() const
{
for (const auto& kv : m_adds) {
if (kv.first == KeySource::UserDefined)
return true;
}
return false;
return std::any_of(m_adds.begin(), m_adds.end(), [](const auto& kv) {
return (kv.first == KeySource::UserDefined);
});
}
void Key::disableAccel(const ui::Accelerator& accel, const KeySource source)
void Key::disableShortcut(const ui::Shortcut& shortcut, const KeySource source)
{
// It doesn't make sense that the default keyboard shortcuts file
// (gui.xml) removes some accelerator.
// (gui.xml) removes some shortcut.
ASSERT(source != KeySource::Original);
erase_accel(m_adds, source, accel);
erase_accel(m_dels, source, accel);
erase_shortcut(m_adds, source, shortcut);
erase_shortcut(m_dels, source, shortcut);
m_dels.emplace_back(source, accel);
m_accels.reset();
m_dels.emplace_back(source, shortcut);
m_shortcuts.reset();
}
void Key::reset()
{
erase_accels(m_adds, KeySource::UserDefined);
erase_accels(m_dels, KeySource::UserDefined);
m_accels.reset();
erase_shortcuts(m_adds, KeySource::UserDefined);
erase_shortcuts(m_dels, KeySource::UserDefined);
m_shortcuts.reset();
}
void Key::copyOriginalToUser()
{
// Erase all user-defined keys
erase_accels(m_adds, KeySource::UserDefined);
erase_accels(m_dels, KeySource::UserDefined);
erase_shortcuts(m_adds, KeySource::UserDefined);
erase_shortcuts(m_dels, KeySource::UserDefined);
// Then copy all original & extension-defined keys as user-defined
auto copy = m_adds;
for (const auto& kv : copy)
m_adds.emplace_back(KeySource::UserDefined, kv.second);
m_accels.reset();
m_shortcuts.reset();
}
std::string Key::triggerString() const
@ -693,21 +687,21 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
// add the keyboard shortcut to the command
KeyPtr key = this->command(command_name, params, keycontext);
if (key && command_key) {
Accelerator accel(command_key);
Shortcut shortcut(command_key);
if (!removed) {
key->add(accel, source, *this);
key->add(shortcut, source, *this);
// Add the shortcut to the menuitems with this command
// (this is only visual, the
// "CustomizedGuiManager::onProcessMessage" is the only
// one that process keyboard shortcuts)
if (key->accels().size() == 1) {
if (key->shortcuts().size() == 1) {
AppMenus::instance()->applyShortcutToMenuitemsWithCommand(command, params, key);
}
}
else
key->disableAccel(accel, source);
key->disableShortcut(shortcut, source);
}
}
}
@ -729,12 +723,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
KeyPtr key = this->tool(tool);
if (key && tool_key) {
LOG(VERBOSE, "KEYS: Shortcut for tool %s: %s\n", tool_id, tool_key);
Accelerator accel(tool_key);
Shortcut shortcut(tool_key);
if (!removed)
key->add(accel, source, *this);
key->add(shortcut, source, *this);
else
key->disableAccel(accel, source);
key->disableShortcut(shortcut, source);
}
}
}
@ -755,12 +749,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
KeyPtr key = this->quicktool(tool);
if (key && tool_key) {
LOG(VERBOSE, "KEYS: Shortcut for quicktool %s: %s\n", tool_id, tool_key);
Accelerator accel(tool_key);
Shortcut shortcut(tool_key);
if (!removed)
key->add(accel, source, *this);
key->add(shortcut, source, *this);
else
key->disableAccel(accel, source);
key->disableShortcut(shortcut, source);
}
}
}
@ -791,12 +785,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
action_id,
(keycontextstr ? keycontextstr : "Any"),
action_key);
Accelerator accel(action_key);
Shortcut shortcut(action_key);
if (!removed)
key->add(accel, source, *this);
key->add(shortcut, source, *this);
else
key->disableAccel(accel, source);
key->disableShortcut(shortcut, source);
}
}
}
@ -817,12 +811,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
KeyPtr key = this->wheelAction(action);
if (key && action_key) {
LOG(VERBOSE, "KEYS: Shortcut for wheel action %s: %s\n", action_id, action_key);
Accelerator accel(action_key);
Shortcut shortcut(action_key);
if (!removed)
key->add(accel, source, *this);
key->add(shortcut, source, *this);
else
key->disableAccel(accel, source);
key->disableShortcut(shortcut, source);
}
}
}
@ -854,12 +848,12 @@ void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
}
LOG(VERBOSE, "KEYS: Shortcut for drag action %s: %s\n", action_id, action_key);
Accelerator accel(action_key);
Shortcut shortcut(action_key);
if (!removed)
key->add(accel, source, *this);
key->add(shortcut, source, *this);
else
key->disableAccel(accel, source);
key->disableShortcut(shortcut, source);
}
}
}
@ -904,24 +898,25 @@ void KeyboardShortcuts::exportFile(const std::string& filename)
void KeyboardShortcuts::exportKeys(XMLElement* parent, KeyType type)
{
for (KeyPtr& key : m_keys) {
// Save only user defined accelerators.
// Save only user defined shortcuts.
if (key->type() != type)
continue;
for (const auto& kv : key->delsKeys())
if (kv.first == KeySource::UserDefined)
exportAccel(parent, key.get(), kv.second, true);
exportShortcut(parent, key.get(), kv.second, true);
for (const auto& kv : key->addsKeys())
if (kv.first == KeySource::UserDefined)
exportAccel(parent, key.get(), kv.second, false);
exportShortcut(parent, key.get(), kv.second, false);
}
}
void KeyboardShortcuts::exportAccel(XMLElement* parent,
const Key* key,
const ui::Accelerator& accel,
bool removed)
// static
void KeyboardShortcuts::exportShortcut(XMLElement* parent,
const Key* key,
const ui::Shortcut& shortcut,
bool removed)
{
XMLElement* elem = parent->InsertNewChildElement("key");
@ -964,7 +959,7 @@ void KeyboardShortcuts::exportAccel(XMLElement* parent,
break;
}
elem->SetAttribute("shortcut", accel.toString().c_str());
elem->SetAttribute("shortcut", shortcut.toString().c_str());
if (removed)
elem->SetAttribute("removed", "true");
@ -1062,27 +1057,28 @@ KeyPtr KeyboardShortcuts::dragAction(const WheelAction dragAction) const
return key;
}
void KeyboardShortcuts::disableAccel(const ui::Accelerator& accel,
const KeySource source,
const KeyContext keyContext,
const Key* newKey)
void KeyboardShortcuts::disableShortcut(const ui::Shortcut& shortcut,
const KeySource source,
const KeyContext keyContext,
const Key* newKey)
{
for (KeyPtr& key : m_keys) {
if (key.get() != newKey && key->keycontext() == keyContext && key->hasAccel(accel) &&
if (key.get() != newKey && key->keycontext() == keyContext && key->hasShortcut(shortcut) &&
// Tools can contain the same keyboard shortcut
(key->type() != KeyType::Tool || newKey == nullptr || newKey->type() != KeyType::Tool) &&
// DragActions can share the same keyboard shortcut (e.g. to
// change different values using different DragVectors)
(key->type() != KeyType::DragAction || newKey == nullptr ||
newKey->type() != KeyType::DragAction)) {
key->disableAccel(accel, source);
key->disableShortcut(shortcut, source);
}
}
}
KeyContext KeyboardShortcuts::getCurrentKeyContext() const
// static
KeyContext KeyboardShortcuts::getCurrentKeyContext()
{
auto ctx = UIContext::instance();
auto* ctx = UIContext::instance();
Doc* doc = ctx->activeDocument();
if (doc && doc->isMaskVisible() &&
// The active key context will be the selectedTool() (in the
@ -1116,7 +1112,7 @@ bool KeyboardShortcuts::getCommandFromKeyMessage(const Message* msg,
int n = (contexts[0] != contexts[1] ? 2 : 1);
for (int i = 0; i < n; ++i) {
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Command && key->isPressed(msg, *this, contexts[i])) {
if (key->type() == KeyType::Command && key->isPressed(msg, contexts[i])) {
if (command)
*command = key->command();
if (params)
@ -1168,12 +1164,12 @@ WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext c
const ui::Message* msg)
{
WheelAction wheelAction = WheelAction::None;
const ui::Accelerator* bestAccel = nullptr;
const ui::Shortcut* bestShortcut = nullptr;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::WheelAction && key->keycontext() == context) {
const ui::Accelerator* accel = key->isPressed(msg, *this);
if ((accel) && (!bestAccel || bestAccel->modifiers() < accel->modifiers())) {
bestAccel = accel;
const ui::Shortcut* shortcut = key->isPressed(msg);
if ((shortcut) && (!bestShortcut || bestShortcut->modifiers() < shortcut->modifiers())) {
bestShortcut = shortcut;
wheelAction = key->wheelAction();
}
}
@ -1181,15 +1177,14 @@ WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext c
return wheelAction;
}
Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const KeyContext context,
const ui::Message* msg)
Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const ui::Message* msg)
{
KeyPtr bestKey = nullptr;
Keys keys;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::DragAction) {
const ui::Accelerator* accel = key->isPressed(msg, *this);
if (accel) {
const ui::Shortcut* shortcut = key->isPressed(msg);
if (shortcut) {
keys.push_back(key);
}
}
@ -1199,11 +1194,9 @@ Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const KeyContext context,
bool KeyboardShortcuts::hasMouseWheelCustomization() const
{
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::WheelAction && key->hasUserDefinedAccels())
return true;
}
return false;
return std::any_of(m_keys.begin(), m_keys.end(), [](const KeyPtr& key) {
return (key->type() == KeyType::WheelAction && key->hasUserDefinedShortcuts());
});
}
void KeyboardShortcuts::clearMouseWheelKeys()
@ -1245,38 +1238,38 @@ void KeyboardShortcuts::setDefaultMouseWheelKeys(const bool zoomWithWheel)
KeyPtr key;
key = std::make_shared<Key>(WheelAction::Zoom);
key->add(Accelerator(zoomWithWheel ? kKeyNoneModifier : kKeyCtrlModifier, kKeyNil, 0),
key->add(Shortcut(zoomWithWheel ? kKeyNoneModifier : kKeyCtrlModifier, kKeyNil, 0),
KeySource::Original,
*this);
m_keys.push_back(key);
if (!zoomWithWheel) {
key = std::make_shared<Key>(WheelAction::VScroll);
key->add(Accelerator(kKeyNoneModifier, kKeyNil, 0), KeySource::Original, *this);
key->add(Shortcut(kKeyNoneModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
}
key = std::make_shared<Key>(WheelAction::HScroll);
key->add(Accelerator(kKeyShiftModifier, kKeyNil, 0), KeySource::Original, *this);
key->add(Shortcut(kKeyShiftModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
key = std::make_shared<Key>(WheelAction::FgColor);
key->add(Accelerator(kKeyAltModifier, kKeyNil, 0), KeySource::Original, *this);
key->add(Shortcut(kKeyAltModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
key = std::make_shared<Key>(WheelAction::BgColor);
key->add(Accelerator((KeyModifiers)(kKeyAltModifier | kKeyShiftModifier), kKeyNil, 0),
key->add(Shortcut((KeyModifiers)(kKeyAltModifier | kKeyShiftModifier), kKeyNil, 0),
KeySource::Original,
*this);
m_keys.push_back(key);
if (zoomWithWheel) {
key = std::make_shared<Key>(WheelAction::BrushSize);
key->add(Accelerator(kKeyCtrlModifier, kKeyNil, 0), KeySource::Original, *this);
key->add(Shortcut(kKeyCtrlModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
key = std::make_shared<Key>(WheelAction::Frame);
key->add(Accelerator((KeyModifiers)(kKeyCtrlModifier | kKeyShiftModifier), kKeyNil, 0),
key->add(Shortcut((KeyModifiers)(kKeyCtrlModifier | kKeyShiftModifier), kKeyNil, 0),
KeySource::Original,
*this);
m_keys.push_back(key);
@ -1321,9 +1314,9 @@ std::string key_tooltip(const char* str, const app::Key* key)
std::string res;
if (str)
res += str;
if (key && !key->accels().empty()) {
if (key && !key->shortcuts().empty()) {
res += " (";
res += key->accels().front().toString();
res += key->shortcuts().front().toString();
res += ")";
}
return res;

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
@ -50,20 +50,20 @@ public:
KeyPtr tool(tools::Tool* tool) const;
KeyPtr quicktool(tools::Tool* tool) const;
KeyPtr action(const KeyAction action, const KeyContext keyContext = KeyContext::Any) const;
KeyPtr wheelAction(const WheelAction action) const;
KeyPtr dragAction(const WheelAction action) const;
KeyPtr wheelAction(WheelAction action) const;
KeyPtr dragAction(WheelAction action) const;
void disableAccel(const ui::Accelerator& accel,
const KeySource source,
const KeyContext keyContext,
const Key* newKey);
void disableShortcut(const ui::Shortcut& shortcut,
KeySource source,
KeyContext keyContext,
const Key* newKey);
KeyContext getCurrentKeyContext() const;
static KeyContext getCurrentKeyContext();
bool getCommandFromKeyMessage(const ui::Message* msg, Command** command, Params* params);
tools::Tool* getCurrentQuicktool(tools::Tool* currentTool);
KeyAction getCurrentActionModifiers(KeyContext context);
WheelAction getWheelActionFromMouseMessage(const KeyContext context, const ui::Message* msg);
Keys getDragActionsFromKeyMessage(const KeyContext context, const ui::Message* msg);
WheelAction getWheelActionFromMouseMessage(KeyContext context, const ui::Message* msg);
Keys getDragActionsFromKeyMessage(const ui::Message* msg);
bool hasMouseWheelCustomization() const;
void clearMouseWheelKeys();
void addMissingMouseWheelKeys();
@ -77,10 +77,10 @@ public:
private:
void exportKeys(tinyxml2::XMLElement* parent, KeyType type);
void exportAccel(tinyxml2::XMLElement* parent,
const Key* key,
const ui::Accelerator& accel,
bool removed);
static void exportShortcut(tinyxml2::XMLElement* parent,
const Key* key,
const ui::Shortcut& shortcut,
bool removed);
mutable Keys m_keys;
};

View File

@ -177,9 +177,6 @@ protected:
ui::Style* style = theme->styles.newsItem();
ui::Style* styleDetail = theme->styles.newsItemDetail();
text::FontMetrics metrics;
font()->metrics(&metrics);
gfx::Size textSize = theme->calcSizeHint(this, style);
gfx::Rect textBounds(bounds.x, bounds.y, bounds.w, textSize.h);
gfx::Rect detailsBounds(bounds.x, bounds.y + textSize.h, bounds.w, bounds.h - textSize.h);
@ -187,19 +184,28 @@ protected:
gfx::Border border = theme->calcBorder(this, style);
gfx::Border borderDetail = theme->calcBorder(this, styleDetail);
PaintWidgetPartInfo info(this);
info.text = &m_title;
info.baseline = border.top() - metrics.ascent;
theme->paintWidgetPart(g, style, bounds, info);
if (!m_titleBlob)
m_titleBlob = text::TextBlob::MakeWithShaper(theme->fontMgr(), font(), m_title);
info.text = &m_desc;
info.baseline += border.bottom() + borderDetail.top() + metrics.descent - metrics.ascent;
theme->paintWidgetPart(g, styleDetail, detailsBounds, info);
PaintWidgetPartInfo info(this);
info.baseline = border.top();
if (m_titleBlob) {
info.textBlob = m_titleBlob;
info.baseline += m_titleBlob->baseline();
theme->paintWidgetPart(g, style, bounds, info);
info.baseline += border.bottom();
info.text = &m_desc;
info.textBlob = nullptr;
info.baseline += borderDetail.top() + m_titleBlob->baseline();
theme->paintWidgetPart(g, styleDetail, detailsBounds, info);
}
}
private:
std::string m_title;
std::string m_desc;
text::TextBlobRef m_titleBlob;
};
class ProblemsItem : public NewsItem {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 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
@ -9,7 +9,7 @@
#include "config.h"
#endif
#include "app/ui/select_accelerator.h"
#include "app/ui/select_shortcut.h"
#include "app/ui/key.h"
#include "app/ui/keyboard_shortcuts.h"
@ -23,23 +23,23 @@ namespace app {
using namespace ui;
class SelectAccelerator::KeyField : public ui::Entry {
class SelectShortcut::KeyField : public ui::Entry {
public:
KeyField(const Accelerator& accel) : ui::Entry(256, "")
KeyField(const Shortcut& shortcut) : ui::Entry(256, "")
{
setTranslateDeadKeys(false);
setExpansive(true);
setFocusMagnet(true);
setAccel(accel);
setShortcut(shortcut);
}
void setAccel(const Accelerator& accel)
void setShortcut(const Shortcut& shortcut)
{
m_accel = accel;
m_shortcut = shortcut;
updateText();
}
obs::signal<void(const ui::Accelerator*)> AccelChange;
obs::signal<void(const ui::Shortcut*)> ShortcutChange;
protected:
bool onProcessMessage(Message* msg) override
@ -56,19 +56,19 @@ protected:
if (keymsg->scancode() == kKeySpace)
modifiers = (KeyModifiers)(modifiers & ~kKeySpaceModifier);
m_accel = Accelerator(
m_shortcut = Shortcut(
modifiers,
keymsg->scancode(),
keymsg->unicodeChar() > 32 ? std::tolower(keymsg->unicodeChar()) : 0);
// Convert the accelerator to a string, and parse it
// again. Just to obtain the exact accelerator we'll read
// Convert the shortcut to a string, and parse it
// again. Just to obtain the exact shortcut we'll read
// when we import the gui.xml file or an .aseprite-keys file.
m_accel = Accelerator(m_accel.toString());
m_shortcut = Shortcut(m_shortcut.toString());
updateText();
AccelChange(&m_accel);
ShortcutChange(&m_shortcut);
return true;
}
break;
@ -78,20 +78,20 @@ protected:
void updateText()
{
setText(
Accelerator(kKeyNoneModifier, m_accel.scancode(), m_accel.unicodeChar()).toString().c_str());
setText(Shortcut(kKeyNoneModifier, m_shortcut.scancode(), m_shortcut.unicodeChar()).toString());
}
Accelerator m_accel;
private:
Shortcut m_shortcut;
};
SelectAccelerator::SelectAccelerator(const ui::Accelerator& accel,
const KeyContext keyContext,
const KeyboardShortcuts& currentKeys)
: m_keyField(new KeyField(accel))
SelectShortcut::SelectShortcut(const ui::Shortcut& shortcut,
const KeyContext keyContext,
const KeyboardShortcuts& currentKeys)
: m_keyField(new KeyField(shortcut))
, m_keyContext(keyContext)
, m_currentKeys(currentKeys)
, m_accel(accel)
, m_shortcut(shortcut)
, m_ok(false)
, m_modified(false)
{
@ -107,7 +107,7 @@ SelectAccelerator::SelectAccelerator(const ui::Accelerator& accel,
space()->Click.connect([this] { onModifierChange(kKeySpaceModifier, space()); });
win()->Click.connect([this] { onModifierChange(kKeyWinModifier, win()); });
m_keyField->AccelChange.connect(&SelectAccelerator::onAccelChange, this);
m_keyField->ShortcutChange.connect(&SelectShortcut::onShortcutChange, this);
clearButton()->Click.connect([this] { onClear(); });
okButton()->Click.connect([this] { onOK(); });
cancelButton()->Click.connect([this] { onCancel(); });
@ -115,63 +115,63 @@ SelectAccelerator::SelectAccelerator(const ui::Accelerator& accel,
addChild(&m_tooltipManager);
}
void SelectAccelerator::onModifierChange(KeyModifiers modifier, CheckBox* checkbox)
void SelectShortcut::onModifierChange(KeyModifiers modifier, CheckBox* checkbox)
{
bool state = (checkbox->isSelected());
KeyModifiers modifiers = m_accel.modifiers();
KeyScancode scancode = m_accel.scancode();
int unicodeChar = m_accel.unicodeChar();
KeyModifiers modifiers = m_shortcut.modifiers();
KeyScancode scancode = m_shortcut.scancode();
int unicodeChar = m_shortcut.unicodeChar();
modifiers = (KeyModifiers)((modifiers & ~modifier) | (state ? modifier : 0));
if (modifiers == kKeySpaceModifier && scancode == kKeySpace)
modifiers = kKeyNoneModifier;
m_accel = Accelerator(modifiers, scancode, unicodeChar);
m_shortcut = Shortcut(modifiers, scancode, unicodeChar);
m_keyField->setAccel(m_accel);
m_keyField->setShortcut(m_shortcut);
m_keyField->requestFocus();
updateAssignedTo();
}
void SelectAccelerator::onAccelChange(const ui::Accelerator* accel)
void SelectShortcut::onShortcutChange(const ui::Shortcut* shortcut)
{
m_accel = *accel;
m_shortcut = *shortcut;
updateModifiers();
updateAssignedTo();
}
void SelectAccelerator::onClear()
void SelectShortcut::onClear()
{
m_accel = Accelerator(kKeyNoneModifier, kKeyNil, 0);
m_shortcut = Shortcut(kKeyNoneModifier, kKeyNil, 0);
m_keyField->setAccel(m_accel);
m_keyField->setShortcut(m_shortcut);
updateModifiers();
updateAssignedTo();
m_keyField->requestFocus();
}
void SelectAccelerator::onOK()
void SelectShortcut::onOK()
{
m_ok = true;
m_modified = (m_origAccel != m_accel);
m_modified = (m_origShortcut != m_shortcut);
closeWindow(NULL);
}
void SelectAccelerator::onCancel()
void SelectShortcut::onCancel()
{
closeWindow(NULL);
}
void SelectAccelerator::updateModifiers()
void SelectShortcut::updateModifiers()
{
alt()->setSelected(m_accel.modifiers() & kKeyAltModifier ? true : false);
ctrl()->setSelected(m_accel.modifiers() & kKeyCtrlModifier ? true : false);
shift()->setSelected(m_accel.modifiers() & kKeyShiftModifier ? true : false);
space()->setSelected(m_accel.modifiers() & kKeySpaceModifier ? true : false);
alt()->setSelected((m_shortcut.modifiers() & kKeyAltModifier) == kKeyAltModifier);
ctrl()->setSelected((m_shortcut.modifiers() & kKeyCtrlModifier) == kKeyCtrlModifier);
shift()->setSelected((m_shortcut.modifiers() & kKeyShiftModifier) == kKeyShiftModifier);
space()->setSelected((m_shortcut.modifiers() & kKeySpaceModifier) == kKeySpaceModifier);
#if __APPLE__
win()->setVisible(false);
cmd()->setSelected(m_accel.modifiers() & kKeyCmdModifier ? true : false);
cmd()->setSelected((m_shortcut.modifiers() & kKeyCmdModifier) == kKeyCmdModifier);
#else
#if __linux__
win()->setText(kWinKeyName);
@ -180,17 +180,17 @@ void SelectAccelerator::updateModifiers()
"Also known as Windows key, logo key,\ncommand key, or system key.",
TOP);
#endif
win()->setSelected(m_accel.modifiers() & kKeyWinModifier ? true : false);
win()->setSelected((m_shortcut.modifiers() & kKeyWinModifier) == kKeyWinModifier);
cmd()->setVisible(false);
#endif
}
void SelectAccelerator::updateAssignedTo()
void SelectShortcut::updateAssignedTo()
{
std::string res = "None";
for (const KeyPtr& key : m_currentKeys) {
if (key->keycontext() == m_keyContext && key->hasAccel(m_accel)) {
if (key->keycontext() == m_keyContext && key->hasShortcut(m_shortcut)) {
res = key->triggerString();
break;
}

View File

@ -1,35 +1,36 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_SELECT_ACCELERATOR_H_INCLUDED
#define APP_UI_SELECT_ACCELERATOR_H_INCLUDED
#ifndef APP_UI_SELECT_SHORTCUT_H_INCLUDED
#define APP_UI_SELECT_SHORTCUT_H_INCLUDED
#pragma once
#include "app/ui/key_context.h"
#include "ui/accelerator.h"
#include "ui/shortcut.h"
#include "ui/tooltips.h"
#include "select_accelerator.xml.h"
#include "select_shortcut.xml.h"
namespace app {
class KeyboardShortcuts;
class SelectAccelerator : public app::gen::SelectAccelerator {
class SelectShortcut : public app::gen::SelectShortcut {
public:
SelectAccelerator(const ui::Accelerator& accelerator,
const KeyContext keyContext,
const KeyboardShortcuts& currentKeys);
SelectShortcut(const ui::Shortcut& shortcut,
KeyContext keyContext,
const KeyboardShortcuts& currentKeys);
bool isOK() const { return m_ok; }
bool isModified() const { return m_modified; }
const ui::Accelerator& accel() const { return m_accel; }
const ui::Shortcut& shortcut() const { return m_shortcut; }
private:
void onModifierChange(ui::KeyModifiers modifier, ui::CheckBox* checkbox);
void onAccelChange(const ui::Accelerator* accel);
void onShortcutChange(const ui::Shortcut* shortcut);
void onClear();
void onOK();
void onCancel();
@ -42,8 +43,8 @@ private:
KeyField* m_keyField;
KeyContext m_keyContext;
const KeyboardShortcuts& m_currentKeys;
ui::Accelerator m_origAccel;
ui::Accelerator m_accel;
ui::Shortcut m_origShortcut;
ui::Shortcut m_shortcut;
bool m_ok;
bool m_modified;
};

View File

@ -42,6 +42,7 @@
#include "text/font.h"
#include "text/font_metrics.h"
#include "text/font_style_set.h"
#include "text/text_blob.h"
#include "ui/intern.h"
#include "ui/ui.h"
@ -257,6 +258,9 @@ static FontData* load_font(const XMLElement* xmlFont, const std::string& xmlFile
if (xmlFont->Attribute("antialias"))
antialias = bool_attr(xmlFont, "antialias", false);
text::FontHinting hinting = (bool_attr(xmlFont, "hinting", true) ? text::FontHinting::Normal :
text::FontHinting::None);
std::string fontFilename;
if (platformFileStr)
fontFilename = app::find_font(xmlDir, platformFileStr);
@ -270,6 +274,7 @@ static FontData* load_font(const XMLElement* xmlFont, const std::string& xmlFile
font->setName(nameStr ? nameStr : (platformFileStr ? platformFileStr : fileStr));
font->setFilename(fontFilename);
font->setAntialias(antialias);
font->setHinting(hinting);
if (!fontFilename.empty())
LOG(VERBOSE, "THEME: Font file '%s' found\n", fontFilename.c_str());
@ -311,6 +316,9 @@ static FontData* load_font(const XMLElement* xmlFont, const std::string& xmlFile
if (!font && systemStr) {
font = try_to_load_system_font(xmlFont);
}
if (font && nameStr)
font->setName(nameStr);
}
else {
throw base::Exception(
@ -536,8 +544,7 @@ void SkinTheme::loadXml(BackwardCompatibility* backward)
float size = 0.0f;
if (const char* sizeStr = xmlFont->Attribute("size"))
size = std::strtof(sizeStr, nullptr);
if (fontData->defaultSize() != 0.0f)
if (size == 0.0f && fontData->defaultSize() != 0.0f)
size = fontData->defaultSize();
const char* mnemonicsStr = xmlFont->Attribute("mnemonics");
@ -545,9 +552,23 @@ void SkinTheme::loadXml(BackwardCompatibility* backward)
text::FontRef font = fontData->getFont(m_fontMgr, size * ui::guiscale());
// SpriteSheetFonts have a default preferred size.
if (size == 0.0f && font->defaultSize() > 0.0f) {
size = font->defaultSize();
// No font?
if (font == nullptr) {
LOG(ERROR, "THEME: Error loading font for theme %s\n", idStr);
xmlFont = xmlFont->NextSiblingElement();
continue;
}
if (size == 0.0f) {
// SpriteSheetFonts have a default preferred size.
if (font->defaultSize() > 0.0f) {
size = font->defaultSize();
}
// For some user extensions, we need to specify at least a
// default size of 10 for TTF theme fonts.
else {
size = 10.0f;
}
font = fontData->getFont(m_fontMgr, size * ui::guiscale());
}
@ -1329,6 +1350,7 @@ public:
, m_h(h)
{
m_widget->getEntryThemeInfo(&m_index, &m_caret, &m_state, &m_range);
m_suffixIndex = m_widget->text().size();
}
int index() const { return m_index; }
@ -1348,6 +1370,11 @@ public:
bg = ColorNone;
fg = colors.text();
// Suffix text
if (m_index >= m_suffixIndex) {
fg = colors.entrySuffix();
}
// Selected
if ((m_index >= m_range.from) && (m_index < m_range.to)) {
if (m_widget->hasFocus())
@ -1412,6 +1439,7 @@ private:
int m_lastX; // Last position used to fill the background
int m_y, m_h;
int m_charStartX;
int m_suffixIndex;
};
} // anonymous namespace
@ -1424,8 +1452,10 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
DrawEntryTextDelegate delegate(widget, g, bounds.origin(), widget->textHeight());
int scroll = delegate.index();
if (!widget->text().empty()) {
const std::string& textString = widget->text();
// Full text to paint: widget text + suffix
const std::string textString = widget->text() + widget->getSuffix();
if (!textString.empty()) {
base::utf8_decode dec(textString);
auto pos = dec.pos();
for (int i = 0; i < scroll && dec.next(); ++i)
@ -1433,41 +1463,28 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
IntersectClip clip(g, bounds);
if (clip) {
text::FontMetrics metrics;
widget->font()->metrics(&metrics);
const float baselineShift = -metrics.ascent - widget->textBlob()->baseline();
int baselineAdjustment = widget->textBaseline();
if (auto blob = widget->textBlob()) {
baselineAdjustment -= blob->baseline();
}
else {
text::FontMetrics metrics;
widget->font()->metrics(&metrics);
baselineAdjustment += metrics.ascent;
}
g->drawTextWithDelegate(std::string(pos, textString.end()), // TODO use a string_view()
colors.text(),
ColorNone,
gfx::Point(bounds.x, bounds.y + baselineShift),
gfx::Point(bounds.x, baselineAdjustment),
&delegate);
}
}
bounds.x += delegate.textBounds().w;
// Draw suffix if there is enough space
if (!widget->getSuffix().empty()) {
Rect sufBounds(bounds.x,
bounds.y,
bounds.x2() - widget->childSpacing() - bounds.x,
widget->textHeight());
IntersectClip clip(g, sufBounds & widget->clientChildrenBounds());
if (clip) {
drawText(g,
widget->getSuffix().c_str(),
colors.entrySuffix(),
ColorNone,
widget,
sufBounds,
widget->align(),
0);
}
}
// Draw caret at the end of the text
if (!delegate.caretDrawn()) {
bounds.x += delegate.textBounds().w;
gfx::Rect charBounds(bounds.x + widget->bounds().x,
bounds.y + widget->bounds().y,
0,
@ -1577,13 +1594,13 @@ void SkinTheme::paintMenuItem(ui::PaintEvent& ev)
}
// Draw the keyboard shortcut
else if (AppMenuItem* appMenuItem = dynamic_cast<AppMenuItem*>(widget)) {
if (appMenuItem->key() && !appMenuItem->key()->accels().empty()) {
if (appMenuItem->key() && !appMenuItem->key()->shortcuts().empty()) {
int old_align = appMenuItem->align();
pos = bounds;
pos.w -= widget->childSpacing() / 4;
std::string buf = appMenuItem->key()->accels().front().toString();
std::string buf = appMenuItem->key()->shortcuts().front().toString();
widget->setAlign(RIGHT | MIDDLE);
drawText(g, buf.c_str(), fg, ColorNone, widget, pos, widget->align(), 0);

View File

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

View File

@ -136,7 +136,7 @@ class StatusBar::Indicators : public HBox {
g->drawText(text(),
textColor,
ColorNone,
Point(rc.x, guiscaled_center(rc.y, rc.h, font()->lineHeight())));
Point(rc.x, guiscaled_center(rc.y, rc.h, textHeight())));
}
}
};
@ -536,9 +536,9 @@ public:
// Tool shortcut
KeyPtr key = KeyboardShortcuts::instance()->tool(tool);
if (key && !key->accels().empty()) {
if (key && !key->shortcuts().empty()) {
add(theme->parts.iconKey(), true);
m_indicators->addTextIndicator(key->accels().front().toString().c_str());
m_indicators->addTextIndicator(key->shortcuts().front().toString().c_str());
}
return *this;
}

View File

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

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 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
@ -110,11 +110,11 @@ std::string AniControls::getTooltipFor(int index) const
tooltip = cmd->friendlyName();
KeyPtr key = KeyboardShortcuts::instance()->command(cmd->id().c_str());
if (!key || key->accels().empty())
if (!key || key->shortcuts().empty())
key = KeyboardShortcuts::instance()->command(cmd->id().c_str(), Params(), KeyContext::Normal);
if (key && !key->accels().empty()) {
if (key && !key->shortcuts().empty()) {
tooltip += "\n\n" + Strings::ani_controls_shortcut() + " ";
tooltip += key->accels().front().toString();
tooltip += key->shortcuts().front().toString();
}
if (index == ACTION_PLAY) {

View File

@ -4321,6 +4321,12 @@ void Timeline::clearAndInvalidateRange()
}
}
void Timeline::refresh()
{
regenerateRows();
invalidate();
}
app::gen::GlobalPref::Timeline& Timeline::timelinePref() const
{
return Preferences::instance().timeline;
@ -4576,7 +4582,7 @@ void Timeline::onDrop(ui::DragEvent& e)
// Determine at which frame and layer the content was dropped on.
frame_t frame = m_frame;
layer_t layerIndex = getLayerIndex(m_layer);
layer_t layerIndex = m_sprite->root()->getLayerIndex(m_layer);
InsertionPoint insert = InsertionPoint::BeforeLayer;
DroppedOn droppedOn = DroppedOn::Unspecified;
TRACE("m_dropRange.type() %d\n", m_dropRange.type());
@ -4602,7 +4608,7 @@ void Timeline::onDrop(ui::DragEvent& e)
break;
case Range::kLayers:
droppedOn = DroppedOn::Layer;
if (m_dropTarget.vhit != DropTarget::VeryBottom) {
if (m_dropTarget.vhit != DropTarget::VeryBottom && !m_dropRange.selectedLayers().empty()) {
auto* selectedLayer = *m_dropRange.selectedLayers().begin();
layerIndex = getLayerIndex(selectedLayer);
}

View File

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

View File

@ -571,9 +571,9 @@ void ToolBar::openTipWindow(int group_index, Tool* tool)
// Tool shortcut
KeyPtr key = KeyboardShortcuts::instance()->tool(tool);
if (key && !key->accels().empty()) {
if (key && !key->shortcuts().empty()) {
tooltip += "\n\n";
tooltip += Strings::tools_shortcut(key->accels().front().toString());
tooltip += Strings::tools_shortcut(key->shortcuts().front().toString());
}
}
else if (group_index == PreviewVisibilityIndex) {

View File

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

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