mirror of https://github.com/aseprite/aseprite.git
Compare commits
61 Commits
07094a0896
...
3788dd269f
Author | SHA1 | Date |
---|---|---|
|
3788dd269f | |
|
af349ce7ee | |
|
3bc4ac0838 | |
|
1774d86939 | |
|
dce1dfd06b | |
|
6283d69707 | |
|
21ad78cdbb | |
|
20b11a0fd3 | |
|
8d4c4857ee | |
|
70c8924719 | |
|
286dd1c755 | |
|
3a14ac72a4 | |
|
2db193b8e3 | |
|
002356ce19 | |
|
9a1e92da35 | |
|
07803ff361 | |
|
8e07617a9d | |
|
9c5ca6bcc6 | |
|
e193891df3 | |
|
bb8547d004 | |
|
85997a08cf | |
|
0995e72a6f | |
|
f61c2c3950 | |
|
0c49f2d7ad | |
|
5f7cc42333 | |
|
8e75cfc4c7 | |
|
983b07383f | |
|
c57554646b | |
|
74953174d6 | |
|
49fa35237a | |
|
0ccf9dcc4f | |
|
194f8424a8 | |
|
debab653fa | |
|
6e9024d54d | |
|
1fa7fd0831 | |
|
ab6b040e83 | |
|
bc312a37b3 | |
|
3129fda977 | |
|
6cb61fb41e | |
|
aa817a8d2a | |
|
40031f83d8 | |
|
90282dbc40 | |
|
1227f9c49c | |
|
d61ae919ad | |
|
b2b2583176 | |
|
b535212642 | |
|
229a3cdf65 | |
|
b3814ec912 | |
|
e88f3bb413 | |
|
eaa2bdf0af | |
|
57309e5aa5 | |
|
4bb9239f50 | |
|
cef92c1a38 | |
|
22e72ab5cb | |
|
80fa065bd5 | |
|
de1ccb24dd | |
|
7d91c4b9d9 | |
|
6d89a6bc15 | |
|
d4e97b5a96 | |
|
205b18dc0f | |
|
56439067b7 |
|
@ -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
|
|
|
@ -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.
|
|
@ -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.
|
8
build.sh
8
build.sh
|
@ -428,9 +428,17 @@ if [ ! -d "$skia_library_dir" ] ; then
|
||||||
skia_url=$(bash laf/misc/skia-url.sh $skia_build)
|
skia_url=$(bash laf/misc/skia-url.sh $skia_build)
|
||||||
skia_file=$(basename $skia_url)
|
skia_file=$(basename $skia_url)
|
||||||
if [ ! -f "$skia_dir/$skia_file" ] ; then
|
if [ ! -f "$skia_dir/$skia_file" ] ; then
|
||||||
|
if ! command -v curl >/dev/null 2>&1 ; then
|
||||||
|
echo "Error: 'curl' command line tool is not available in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
curl --ssl-revoke-best-effort -L -o "$skia_dir/$skia_file" "$skia_url"
|
curl --ssl-revoke-best-effort -L -o "$skia_dir/$skia_file" "$skia_url"
|
||||||
fi
|
fi
|
||||||
if [ ! -d "$skia_library_dir" ] ; then
|
if [ ! -d "$skia_library_dir" ] ; then
|
||||||
|
if ! command -v unzip >/dev/null 2>&1 ; then
|
||||||
|
echo "Error: 'unzip' command line tool is not available in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
unzip -n -d "$skia_dir" "$skia_dir/$skia_file"
|
unzip -n -d "$skia_dir" "$skia_dir/$skia_file"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Aseprite -->
|
<!-- Aseprite -->
|
||||||
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
|
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
|
||||||
<!-- Copyright (C) 2001-2018 David Capello -->
|
<!-- Copyright (C) 2001-2018 David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<!-- Keyboard shortcuts -->
|
<!-- Keyboard shortcuts -->
|
||||||
|
@ -371,6 +371,12 @@
|
||||||
<param name="quantity" value="1" />
|
<param name="quantity" value="1" />
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
|
<!-- Main selection actions (apply transformation / undo) -->
|
||||||
|
<key command="DeselectMask" shortcut="Esc" context="Transformation" />
|
||||||
|
<key command="Apply" shortcut="Enter" context="Transformation" />
|
||||||
|
<key command="Apply" shortcut="Enter Pad" context="Transformation" />
|
||||||
|
<key command="Undo" shortcut="Ctrl+Z" mac="Cmd+Z" context="Transformation" />
|
||||||
|
|
||||||
<!-- Move selection with arrows -->
|
<!-- Move selection with arrows -->
|
||||||
<key command="MoveMask" shortcut="Left" context="Selection">
|
<key command="MoveMask" shortcut="Left" context="Selection">
|
||||||
<param name="target" value="content" />
|
<param name="target" value="content" />
|
||||||
|
@ -1203,6 +1209,7 @@
|
||||||
|
|
||||||
<menu id="slice_popup_menu">
|
<menu id="slice_popup_menu">
|
||||||
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
|
<item command="SliceProperties" text="@.properties" group="slice_popup_properties" />
|
||||||
|
<item command="DuplicateSlice" text="@.duplicate" group="slice_popup_duplicate" />
|
||||||
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
|
<item command="RemoveSlice" text="@.delete" group="slice_popup_delete" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
|
|
|
@ -206,6 +206,7 @@ AddColor_Background = Background
|
||||||
AddColor_Foreground = Foreground
|
AddColor_Foreground = Foreground
|
||||||
AddColor_Specific = Specific
|
AddColor_Specific = Specific
|
||||||
AdvancedMode = Advanced Mode
|
AdvancedMode = Advanced Mode
|
||||||
|
Apply = Apply
|
||||||
AutocropSprite = Trim Sprite
|
AutocropSprite = Trim Sprite
|
||||||
AutocropSprite_ByGrid = Trim Sprite by Grid
|
AutocropSprite_ByGrid = Trim Sprite by Grid
|
||||||
BackgroundFromLayer = Background from Layer
|
BackgroundFromLayer = Background from Layer
|
||||||
|
@ -265,6 +266,7 @@ Despeckle = Despeckle
|
||||||
DeveloperConsole = Developer Console
|
DeveloperConsole = Developer Console
|
||||||
DiscardBrush = Discard Brush
|
DiscardBrush = Discard Brush
|
||||||
DuplicateLayer = Duplicate Layer
|
DuplicateLayer = Duplicate Layer
|
||||||
|
DuplicateSlice = Duplicate Slice
|
||||||
DuplicateSprite = Duplicate Sprite
|
DuplicateSprite = Duplicate Sprite
|
||||||
DuplicateView = Duplicate View
|
DuplicateView = Duplicate View
|
||||||
Exit = Exit
|
Exit = Exit
|
||||||
|
@ -581,8 +583,9 @@ rotsprite = RotSprite
|
||||||
pixel_perfect = Pixel-perfect
|
pixel_perfect = Pixel-perfect
|
||||||
linear_gradient = Linear Gradient
|
linear_gradient = Linear Gradient
|
||||||
radial_gradient = Radial Gradient
|
radial_gradient = Radial Gradient
|
||||||
drop_pixel = Drop pixels here (Enter)
|
drop_pixel_and_deselect = Apply transformation and deselect
|
||||||
cancel_drag = Cancel drag and drop (Esc)
|
drop_pixel = Apply transformation and keep selection
|
||||||
|
cancel_drag = Cancel transformation and undo/discard changes
|
||||||
auto_select_layer = Auto Select Layer
|
auto_select_layer = Auto Select Layer
|
||||||
all = All
|
all = All
|
||||||
none = None
|
none = None
|
||||||
|
@ -621,6 +624,14 @@ current_layer = Current Layer
|
||||||
first_ref_layer = First Reference Layer
|
first_ref_layer = First Reference Layer
|
||||||
pick = Pick:
|
pick = Pick:
|
||||||
sample = Sample:
|
sample = Sample:
|
||||||
|
position_label = P:
|
||||||
|
rotation_label = R:
|
||||||
|
position_x = X Position
|
||||||
|
position_y = Y Position
|
||||||
|
size_width = Width
|
||||||
|
size_height = Height
|
||||||
|
rotation_angle = Angle
|
||||||
|
rotation_skew = Skew
|
||||||
|
|
||||||
[convolution_matrix]
|
[convolution_matrix]
|
||||||
reload_stock = &Reload Stock
|
reload_stock = &Reload Stock
|
||||||
|
@ -789,7 +800,22 @@ empty_fonts = No system fonts were found
|
||||||
[font_style]
|
[font_style]
|
||||||
antialias = Antialias
|
antialias = Antialias
|
||||||
hinting = Hinting
|
hinting = Hinting
|
||||||
|
hinting_none = No Hinting
|
||||||
|
hinting_slight = Slight Hinting
|
||||||
|
hinting_full = Full Hinting
|
||||||
ligatures = Ligatures
|
ligatures = Ligatures
|
||||||
|
font_weight = Font Weight
|
||||||
|
italic = Italic
|
||||||
|
font_weight_100 = Thin
|
||||||
|
font_weight_200 = Extra Light
|
||||||
|
font_weight_300 = Light
|
||||||
|
font_weight_400 = Normal
|
||||||
|
font_weight_500 = Medium
|
||||||
|
font_weight_600 = Semi Bold
|
||||||
|
font_weight_700 = Bold
|
||||||
|
font_weight_800 = Extra Bold
|
||||||
|
font_weight_900 = Black
|
||||||
|
font_weight_1000 = Extra Black
|
||||||
|
|
||||||
[frame_combo]
|
[frame_combo]
|
||||||
all_frames = All frames
|
all_frames = All frames
|
||||||
|
@ -817,6 +843,7 @@ same_in_all_tools = Same in all Tools
|
||||||
opacity = Opacity:
|
opacity = Opacity:
|
||||||
tolerance = Tolerance:
|
tolerance = Tolerance:
|
||||||
show_more = Show more...
|
show_more = Show more...
|
||||||
|
copy_of = {} Copy
|
||||||
|
|
||||||
[general_text]
|
[general_text]
|
||||||
copy = &Copy
|
copy = &Copy
|
||||||
|
@ -957,6 +984,7 @@ key_context_move_tool = Move Tool
|
||||||
key_context_freehand_tool = Freehand Tool
|
key_context_freehand_tool = Freehand Tool
|
||||||
key_context_shape_tool = Shape Tool
|
key_context_shape_tool = Shape Tool
|
||||||
key_context_frames_selection = Frames Selection
|
key_context_frames_selection = Frames Selection
|
||||||
|
key_context_transformation = Transformation
|
||||||
copy_selection = Copy Selection
|
copy_selection = Copy Selection
|
||||||
snap_to_grid = Snap To Grid
|
snap_to_grid = Snap To Grid
|
||||||
lock_axis = Lock Axis
|
lock_axis = Lock Axis
|
||||||
|
@ -1664,6 +1692,10 @@ from = From:
|
||||||
to = To:
|
to = To:
|
||||||
tolerance = Tolerance:
|
tolerance = Tolerance:
|
||||||
|
|
||||||
|
[duplicate_slice]
|
||||||
|
x_duplicated = Slice "{}" duplicated
|
||||||
|
n_slices_duplicated = {} slice(s) duplicated
|
||||||
|
|
||||||
[remove_slice]
|
[remove_slice]
|
||||||
x_removed = Slice "{}" removed
|
x_removed = Slice "{}" removed
|
||||||
n_slices_removed = {} slice(s) removed
|
n_slices_removed = {} slice(s) removed
|
||||||
|
@ -1748,6 +1780,7 @@ delete_file = Delete file, I've already sent it
|
||||||
|
|
||||||
[slice_popup_menu]
|
[slice_popup_menu]
|
||||||
properties = Slice &Properties...
|
properties = Slice &Properties...
|
||||||
|
duplicate = D&uplicate Slice
|
||||||
delete = &Delete Slice
|
delete = &Delete Slice
|
||||||
|
|
||||||
[slice_properties]
|
[slice_properties]
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Aseprite -->
|
<!-- Aseprite -->
|
||||||
<!-- Copyright (C) 2019-2024 Igara Studio S.A. -->
|
<!-- Copyright (C) 2019-2025 Igara Studio S.A. -->
|
||||||
<!-- Copyright (C) 2017-2018 David Capello -->
|
<!-- Copyright (C) 2017-2018 David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<window id="slice_properties" text="@.title" help="slices#slice-properties">
|
<window id="slice_properties" text="@.title" help="slices#slice-properties">
|
||||||
<vbox>
|
<vbox expansive="true">
|
||||||
<grid id="properties_grid" columns="3">
|
<grid id="properties_grid" columns="3">
|
||||||
<label id="label1" text="@.name" />
|
<label id="label1" text="@.name" />
|
||||||
<entry id="name" maxsize="256" magnet="true" cell_align="horizontal" expansive="true" />
|
<entry id="name" maxsize="256" magnet="true" cell_align="horizontal" expansive="true" />
|
||||||
<button id="user_data" icon="icon_user_data" maxsize="32" tooltip="@.user_data_tooltip" />
|
<button id="user_data" icon="icon_user_data" maxsize="32" tooltip="@.user_data_tooltip" />
|
||||||
</grid>
|
</grid>
|
||||||
<grid columns="2">
|
<grid columns="3" expansive="true">
|
||||||
<separator horizontal="true" cell_hspan="2" />
|
<separator horizontal="true" cell_hspan="3" />
|
||||||
|
|
||||||
<box />
|
<box />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
|
@ -20,6 +20,7 @@
|
||||||
<label text="@.width" />
|
<label text="@.width" />
|
||||||
<label text="@.height" />
|
<label text="@.height" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller cell_align="horizontal" />
|
||||||
|
|
||||||
<label text="@.bounds" />
|
<label text="@.bounds" />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
<expr id="bounds_w" />
|
<expr id="bounds_w" />
|
||||||
<expr id="bounds_h" />
|
<expr id="bounds_h" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller />
|
||||||
|
|
||||||
<check text="@.center" id="center" />
|
<check text="@.center" id="center" />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
|
@ -36,16 +38,18 @@
|
||||||
<expr id="center_w" />
|
<expr id="center_w" />
|
||||||
<expr id="center_h" />
|
<expr id="center_h" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller />
|
||||||
|
|
||||||
<check text="@.pivot" id="pivot" />
|
<check text="@.pivot" id="pivot" />
|
||||||
<hbox>
|
<hbox>
|
||||||
<expr id="pivot_x" />
|
<expr id="pivot_x" />
|
||||||
<expr id="pivot_y" />
|
<expr id="pivot_y" />
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<boxfiller />
|
||||||
|
|
||||||
<separator horizontal="true" cell_hspan="2" />
|
<boxfiller cell_align="vertical" cell_hspan="3" />
|
||||||
|
<separator horizontal="true" cell_hspan="3" cell_align="horizontal" />
|
||||||
<hbox cell_hspan="2">
|
<hbox cell_hspan="3">
|
||||||
<boxfiller />
|
<boxfiller />
|
||||||
<hbox homogeneous="true">
|
<hbox homogeneous="true">
|
||||||
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Aseprite -->
|
<!-- Aseprite -->
|
||||||
<!-- Copyright (C) 2019-2021 Igara Studio S.A. -->
|
<!-- Copyright (C) 2019-2025 Igara Studio S.A. -->
|
||||||
<!-- Copyright (C) 2015-2018 David Capello -->
|
<!-- Copyright (C) 2015-2018 David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<window id="tag_properties" text="@.title">
|
<window id="tag_properties" text="@.title">
|
||||||
|
@ -23,13 +23,14 @@
|
||||||
<check text="@.repeat" id="limit_repeat" />
|
<check text="@.repeat" id="limit_repeat" />
|
||||||
<vbox id="repeat_placeholder" cell_hspan="2" />
|
<vbox id="repeat_placeholder" cell_hspan="2" />
|
||||||
</grid>
|
</grid>
|
||||||
|
<boxfiller />
|
||||||
<grid columns="2">
|
<grid columns="2">
|
||||||
<separator horizontal="true" cell_hspan="2" minwidth="180" />
|
<separator horizontal="true" cell_align="horizontal" cell_hspan="2" minwidth="180" />
|
||||||
|
|
||||||
<box horizontal="true" homogeneous="true" cell_hspan="2" cell_align="right">
|
<hbox homogeneous="true" cell_hspan="2" cell_align="right">
|
||||||
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
||||||
<button text="@general.cancel" closewindow="true" />
|
<button text="@general.cancel" closewindow="true" />
|
||||||
</box>
|
</hbox>
|
||||||
</grid>
|
</grid>
|
||||||
</vbox>
|
</vbox>
|
||||||
</window>
|
</window>
|
||||||
|
|
2
laf
2
laf
|
@ -1 +1 @@
|
||||||
Subproject commit a2bb9ec7fb98354279a2c49870a4a47a67a8e86e
|
Subproject commit 3f1f86cc734443ba5c72d25c72cdf41ca208e9fe
|
|
@ -180,8 +180,8 @@ if(ENABLE_ASEPRITE_EXE)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(main_resources
|
set(main_resources
|
||||||
main/resources_win32.rc
|
main/win/resources_win32.rc
|
||||||
main/settings.manifest)
|
main/win/settings.manifest)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(${main_target}
|
add_executable(${main_target}
|
||||||
|
|
|
@ -74,7 +74,7 @@ add_custom_command(
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${output_fn}.tmp ${output_fn}
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${output_fn}.tmp ${output_fn}
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
MAIN_DEPENDENCY ${strings_en_ini}
|
MAIN_DEPENDENCY ${strings_en_ini}
|
||||||
DEPENDS ${GEN_DEP})
|
DEPENDS ${GEN_DEP} commands/commands_list.h)
|
||||||
list(APPEND generated_files ${output_fn})
|
list(APPEND generated_files ${output_fn})
|
||||||
|
|
||||||
# Check translations
|
# Check translations
|
||||||
|
@ -368,6 +368,7 @@ target_sources(app-lib PRIVATE
|
||||||
color_picker.cpp
|
color_picker.cpp
|
||||||
color_spaces.cpp
|
color_spaces.cpp
|
||||||
color_utils.cpp
|
color_utils.cpp
|
||||||
|
commands/apply.cpp
|
||||||
commands/cmd_about.cpp
|
commands/cmd_about.cpp
|
||||||
commands/cmd_add_color.cpp
|
commands/cmd_add_color.cpp
|
||||||
commands/cmd_advanced_mode.cpp
|
commands/cmd_advanced_mode.cpp
|
||||||
|
@ -393,6 +394,7 @@ target_sources(app-lib PRIVATE
|
||||||
commands/cmd_deselect_mask.cpp
|
commands/cmd_deselect_mask.cpp
|
||||||
commands/cmd_discard_brush.cpp
|
commands/cmd_discard_brush.cpp
|
||||||
commands/cmd_duplicate_layer.cpp
|
commands/cmd_duplicate_layer.cpp
|
||||||
|
commands/cmd_duplicate_slice.cpp
|
||||||
commands/cmd_duplicate_sprite.cpp
|
commands/cmd_duplicate_sprite.cpp
|
||||||
commands/cmd_duplicate_view.cpp
|
commands/cmd_duplicate_view.cpp
|
||||||
commands/cmd_enter_license.cpp
|
commands/cmd_enter_license.cpp
|
||||||
|
@ -651,6 +653,7 @@ target_sources(app-lib PRIVATE
|
||||||
ui/icon_button.cpp
|
ui/icon_button.cpp
|
||||||
ui/incompat_file_window.cpp
|
ui/incompat_file_window.cpp
|
||||||
ui/input_chain.cpp
|
ui/input_chain.cpp
|
||||||
|
ui/key.cpp
|
||||||
ui/keyboard_shortcuts.cpp
|
ui/keyboard_shortcuts.cpp
|
||||||
ui/layer_frame_comboboxes.cpp
|
ui/layer_frame_comboboxes.cpp
|
||||||
ui/main_menu_bar.cpp
|
ui/main_menu_bar.cpp
|
||||||
|
@ -715,6 +718,7 @@ target_sources(app-lib PRIVATE
|
||||||
util/render_text.cpp
|
util/render_text.cpp
|
||||||
util/resize_image.cpp
|
util/resize_image.cpp
|
||||||
util/shader_helpers.cpp
|
util/shader_helpers.cpp
|
||||||
|
util/slice_utils.cpp
|
||||||
util/tile_flags_utils.cpp
|
util/tile_flags_utils.cpp
|
||||||
util/tileset_utils.cpp
|
util/tileset_utils.cpp
|
||||||
util/wrap_point.cpp
|
util/wrap_point.cpp
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "app/commands/command.h"
|
||||||
|
#include "app/context.h"
|
||||||
|
#include "app/ui/editor/editor.h"
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Depends on the current context/state, used to apply the current
|
||||||
|
// transformation (drop pixels).
|
||||||
|
class ApplyCommand : public Command {
|
||||||
|
public:
|
||||||
|
ApplyCommand();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onExecute(Context* ctx) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
ApplyCommand::ApplyCommand() : Command(CommandId::Apply(), CmdUIOnlyFlag)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyCommand::onExecute(Context* ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->isUIAvailable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto* editor = Editor::activeEditor();
|
||||||
|
if (editor && editor->isMovingPixels())
|
||||||
|
editor->dropMovingPixels();
|
||||||
|
}
|
||||||
|
|
||||||
|
Command* CommandFactory::createApplyCommand()
|
||||||
|
{
|
||||||
|
return new ApplyCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2023 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -101,12 +101,12 @@ public:
|
||||||
|
|
||||||
if (countCels() > 0) {
|
if (countCels() > 0) {
|
||||||
m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()),
|
m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()),
|
||||||
g_window->propertiesGrid());
|
propertiesGrid());
|
||||||
}
|
}
|
||||||
else if (!m_cel)
|
else if (!m_cel)
|
||||||
m_userDataView.setVisible(false, false);
|
m_userDataView.setVisible(false, false);
|
||||||
|
|
||||||
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
updateFromCel();
|
updateFromCel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ private:
|
||||||
{
|
{
|
||||||
if (countCels() > 0) {
|
if (countCels() > 0) {
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -269,7 +269,7 @@ private:
|
||||||
|
|
||||||
if (m_key && m_key->keycontext() != KeyContext::Any) {
|
if (m_key && m_key->keycontext() != KeyContext::Any) {
|
||||||
int w = m_headerItem->contextXPos() +
|
int w = m_headerItem->contextXPos() +
|
||||||
font()->textLength(convertKeyContextToUserFriendlyString(m_key->keycontext()));
|
font()->textLength(convert_keycontext_to_user_friendly_string(m_key->keycontext()));
|
||||||
size.w = std::max(size.w, w);
|
size.w = std::max(size.w, w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +317,7 @@ private:
|
||||||
|
|
||||||
if (m_key && !m_key->shortcuts().empty()) {
|
if (m_key && !m_key->shortcuts().empty()) {
|
||||||
if (m_key->keycontext() != KeyContext::Any) {
|
if (m_key->keycontext() != KeyContext::Any) {
|
||||||
g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()),
|
g->drawText(convert_keycontext_to_user_friendly_string(m_key->keycontext()),
|
||||||
fg,
|
fg,
|
||||||
bg,
|
bg,
|
||||||
gfx::Point(contextXPos, y));
|
gfx::Point(contextXPos, y));
|
||||||
|
@ -595,7 +595,7 @@ private:
|
||||||
case KeyContext::MoveTool:
|
case KeyContext::MoveTool:
|
||||||
case KeyContext::FreehandTool:
|
case KeyContext::FreehandTool:
|
||||||
case KeyContext::ShapeTool:
|
case KeyContext::ShapeTool:
|
||||||
text = convertKeyContextToUserFriendlyString(key->keycontext()) + ": " + text;
|
text = convert_keycontext_to_user_friendly_string(key->keycontext()) + ": " + text;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key, nullptr, 0, &m_headerItem);
|
KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key, nullptr, 0, &m_headerItem);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2024 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -166,7 +166,7 @@ public:
|
||||||
m_document->add_observer(this);
|
m_document->add_observer(this);
|
||||||
|
|
||||||
if (countLayers() > 0) {
|
if (countLayers() > 0) {
|
||||||
m_userDataView.configureAndSet(m_layer->userData(), g_window->propertiesGrid());
|
m_userDataView.configureAndSet(m_layer->userData(), propertiesGrid());
|
||||||
if (m_remapAfterConfigure) {
|
if (m_remapAfterConfigure) {
|
||||||
remapWindow();
|
remapWindow();
|
||||||
centerWindow();
|
centerWindow();
|
||||||
|
@ -368,8 +368,7 @@ private:
|
||||||
{
|
{
|
||||||
if (m_layer) {
|
if (m_layer) {
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
g_window->remapWindow();
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
manager()->invalidate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -267,16 +267,8 @@ void NewLayerCommand::onExecute(Context* context)
|
||||||
bool afterBackground = false;
|
bool afterBackground = false;
|
||||||
|
|
||||||
switch (m_type) {
|
switch (m_type) {
|
||||||
case Type::Layer:
|
case Type::Layer: layer = api.newLayer(parent, name); break;
|
||||||
|
case Type::Group: layer = api.newGroup(parent, name); break;
|
||||||
if (m_place == Place::BeforeActiveLayer) {
|
|
||||||
layer = api.newLayer(parent, name);
|
|
||||||
api.restackLayerBefore(layer, parent, activeLayer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
layer = api.newLayerAfter(parent, name, activeLayer);
|
|
||||||
break;
|
|
||||||
case Type::Group: layer = api.newGroupAfter(parent, name, activeLayer); break;
|
|
||||||
case Type::ReferenceLayer:
|
case Type::ReferenceLayer:
|
||||||
layer = api.newLayer(parent, name);
|
layer = api.newLayer(parent, name);
|
||||||
if (layer)
|
if (layer)
|
||||||
|
@ -311,6 +303,15 @@ void NewLayerCommand::onExecute(Context* context)
|
||||||
|
|
||||||
ASSERT(layer->parent());
|
ASSERT(layer->parent());
|
||||||
|
|
||||||
|
// Reorder the resulting layer.
|
||||||
|
switch (m_place) {
|
||||||
|
case Place::AfterActiveLayer: api.restackLayerAfter(layer, parent, activeLayer); break;
|
||||||
|
case Place::BeforeActiveLayer: api.restackLayerBefore(layer, parent, activeLayer); break;
|
||||||
|
case Place::Top:
|
||||||
|
api.restackLayerAfter(layer, sprite->root(), sprite->root()->lastLayer());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Put new layer as an overlay of the background or in the first
|
// Put new layer as an overlay of the background or in the first
|
||||||
// layer in case the sprite is transparent.
|
// layer in case the sprite is transparent.
|
||||||
if (afterBackground) {
|
if (afterBackground) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -188,8 +188,7 @@ private:
|
||||||
void onToggleUserData()
|
void onToggleUserData()
|
||||||
{
|
{
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
remapWindow();
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
manager()->invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTilesedDuplicated(const Tileset* tilesetClone)
|
void onTilesedDuplicated(const Tileset* tilesetClone)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2023 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -8,6 +8,7 @@
|
||||||
FOR_EACH_COMMAND(About)
|
FOR_EACH_COMMAND(About)
|
||||||
FOR_EACH_COMMAND(AddColor)
|
FOR_EACH_COMMAND(AddColor)
|
||||||
FOR_EACH_COMMAND(AdvancedMode)
|
FOR_EACH_COMMAND(AdvancedMode)
|
||||||
|
FOR_EACH_COMMAND(Apply)
|
||||||
FOR_EACH_COMMAND(AutocropSprite)
|
FOR_EACH_COMMAND(AutocropSprite)
|
||||||
FOR_EACH_COMMAND(BackgroundFromLayer)
|
FOR_EACH_COMMAND(BackgroundFromLayer)
|
||||||
FOR_EACH_COMMAND(BrightnessContrast)
|
FOR_EACH_COMMAND(BrightnessContrast)
|
||||||
|
@ -40,6 +41,7 @@ FOR_EACH_COMMAND(DeselectMask)
|
||||||
FOR_EACH_COMMAND(Despeckle)
|
FOR_EACH_COMMAND(Despeckle)
|
||||||
FOR_EACH_COMMAND(DiscardBrush)
|
FOR_EACH_COMMAND(DiscardBrush)
|
||||||
FOR_EACH_COMMAND(DuplicateLayer)
|
FOR_EACH_COMMAND(DuplicateLayer)
|
||||||
|
FOR_EACH_COMMAND(DuplicateSlice)
|
||||||
FOR_EACH_COMMAND(DuplicateSprite)
|
FOR_EACH_COMMAND(DuplicateSprite)
|
||||||
FOR_EACH_COMMAND(DuplicateView)
|
FOR_EACH_COMMAND(DuplicateView)
|
||||||
FOR_EACH_COMMAND(Exit)
|
FOR_EACH_COMMAND(Exit)
|
||||||
|
|
|
@ -81,7 +81,7 @@ public:
|
||||||
|
|
||||||
~ConsoleWindow() { TRACE_CON("CON: ~ConsoleWindow this=", this); }
|
~ConsoleWindow() { TRACE_CON("CON: ~ConsoleWindow this=", this); }
|
||||||
|
|
||||||
void addMessage(const std::string& msg)
|
void addMessage(std::string msg)
|
||||||
{
|
{
|
||||||
if (!m_hasText) {
|
if (!m_hasText) {
|
||||||
m_hasText = true;
|
m_hasText = true;
|
||||||
|
@ -93,6 +93,17 @@ public:
|
||||||
gfx::Point pt = m_view.viewScroll();
|
gfx::Point pt = m_view.viewScroll();
|
||||||
const bool autoScroll = (pt.y >= maxSize.h - visible.h);
|
const bool autoScroll = (pt.y >= maxSize.h - visible.h);
|
||||||
|
|
||||||
|
// Escape characters we can't show properly
|
||||||
|
for (size_t i = 0; i < msg.size(); i++) {
|
||||||
|
switch (msg[i]) {
|
||||||
|
case '\a':
|
||||||
|
case '\b':
|
||||||
|
case '\r':
|
||||||
|
case '\t':
|
||||||
|
case '\v': msg[i] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_textbox.setText(m_textbox.text() + msg);
|
m_textbox.setText(m_textbox.text() + msg);
|
||||||
|
|
||||||
if (autoScroll) {
|
if (autoScroll) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -338,6 +338,19 @@ void Doc::notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti)
|
||||||
notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev);
|
notify_observers<DocEvent&>(&DocObserver::onAfterAddTile, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Doc::notifyBeforeSlicesDuplication()
|
||||||
|
{
|
||||||
|
DocEvent ev(this);
|
||||||
|
notify_observers<DocEvent&>(&DocObserver::onBeforeSlicesDuplication, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Doc::notifySliceDuplicated(Slice* slice)
|
||||||
|
{
|
||||||
|
DocEvent ev(this);
|
||||||
|
ev.slice(slice);
|
||||||
|
notify_observers<DocEvent&>(&DocObserver::onSliceDuplicated, ev);
|
||||||
|
}
|
||||||
|
|
||||||
bool Doc::isModified() const
|
bool Doc::isModified() const
|
||||||
{
|
{
|
||||||
return !m_undo->isInSavedStateOrSimilar();
|
return !m_undo->isInSavedStateOrSimilar();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -141,6 +141,8 @@ public:
|
||||||
void notifyTilesetChanged(Tileset* tileset);
|
void notifyTilesetChanged(Tileset* tileset);
|
||||||
void notifyLayerGroupCollapseChange(Layer* layer);
|
void notifyLayerGroupCollapseChange(Layer* layer);
|
||||||
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
|
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
|
||||||
|
void notifyBeforeSlicesDuplication();
|
||||||
|
void notifySliceDuplicated(Slice* slice);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// File related properties
|
// File related properties
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -90,6 +90,8 @@ public:
|
||||||
|
|
||||||
// Slices
|
// Slices
|
||||||
virtual void onSliceNameChange(DocEvent& ev) {}
|
virtual void onSliceNameChange(DocEvent& ev) {}
|
||||||
|
virtual void onBeforeSlicesDuplication(DocEvent& ev) {}
|
||||||
|
virtual void onSliceDuplicated(DocEvent& ev) {}
|
||||||
|
|
||||||
// The tileset has changed.
|
// The tileset has changed.
|
||||||
virtual void onTilesetChanged(DocEvent& ev) {}
|
virtual void onTilesetChanged(DocEvent& ev) {}
|
||||||
|
|
|
@ -1095,6 +1095,9 @@ public:
|
||||||
gifframe_t nframes = totalFrames();
|
gifframe_t nframes = totalFrames();
|
||||||
for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) {
|
for (gifframe_t gifFrame = 0; gifFrame < nframes; ++gifFrame) {
|
||||||
ASSERT(frame_it != frame_end);
|
ASSERT(frame_it != frame_end);
|
||||||
|
if (m_fop->isStop())
|
||||||
|
break;
|
||||||
|
|
||||||
frame_t frame = *frame_it;
|
frame_t frame = *frame_it;
|
||||||
++frame_it;
|
++frame_it;
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "app/fonts/font_info.h"
|
#include "app/fonts/font_info.h"
|
||||||
|
|
||||||
#include "app/fonts/font_data.h"
|
#include "app/fonts/font_data.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
#include "base/split_string.h"
|
#include "base/split_string.h"
|
||||||
|
@ -142,19 +143,22 @@ std::string FontInfo::humanString() const
|
||||||
}
|
}
|
||||||
result += fmt::format(" {}pt", size());
|
result += fmt::format(" {}pt", size());
|
||||||
if (!result.empty()) {
|
if (!result.empty()) {
|
||||||
if (style().weight() >= text::FontStyle::Weight::SemiBold)
|
if (style().weight() != text::FontStyle::Weight::Normal)
|
||||||
result += " Bold";
|
result +=
|
||||||
|
" " +
|
||||||
|
Strings::Translate(
|
||||||
|
fmt::format("font_style.font_weight_{}", static_cast<int>(style().weight())).c_str());
|
||||||
if (style().slant() != text::FontStyle::Slant::Upright)
|
if (style().slant() != text::FontStyle::Slant::Upright)
|
||||||
result += " Italic";
|
result += " " + Strings::font_style_italic();
|
||||||
if (antialias())
|
if (antialias())
|
||||||
result += " Antialias";
|
result += " " + Strings::font_style_antialias();
|
||||||
if (ligatures())
|
if (ligatures())
|
||||||
result += " Ligatures";
|
result += " " + Strings::font_style_ligatures();
|
||||||
switch (hinting()) {
|
switch (hinting()) {
|
||||||
case text::FontHinting::None: result += " No Hinting"; break;
|
case text::FontHinting::None: result += " " + Strings::font_style_hinting_none(); break;
|
||||||
case text::FontHinting::Slight: result += " Slight Hinting"; break;
|
case text::FontHinting::Slight: result += " " + Strings::font_style_hinting_slight(); break;
|
||||||
case text::FontHinting::Normal: break;
|
case text::FontHinting::Normal: break;
|
||||||
case text::FontHinting::Full: result += " Full Hinting"; break;
|
case text::FontHinting::Full: result += " " + Strings::font_style_hinting_full(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -177,6 +181,7 @@ app::FontInfo convert_to(const std::string& from)
|
||||||
bool italic = false;
|
bool italic = false;
|
||||||
app::FontInfo::Flags flags = app::FontInfo::Flags::None;
|
app::FontInfo::Flags flags = app::FontInfo::Flags::None;
|
||||||
text::FontHinting hinting = text::FontHinting::Normal;
|
text::FontHinting hinting = text::FontHinting::Normal;
|
||||||
|
text::FontStyle::Weight weight = text::FontStyle::Weight::Normal;
|
||||||
|
|
||||||
if (!parts.empty()) {
|
if (!parts.empty()) {
|
||||||
if (parts[0].compare(0, 5, "file=") == 0) {
|
if (parts[0].compare(0, 5, "file=") == 0) {
|
||||||
|
@ -214,16 +219,17 @@ app::FontInfo convert_to(const std::string& from)
|
||||||
else if (hintingStr == "full")
|
else if (hintingStr == "full")
|
||||||
hinting = text::FontHinting::Full;
|
hinting = text::FontHinting::Full;
|
||||||
}
|
}
|
||||||
|
else if (parts[i].compare(0, 7, "weight=") == 0) {
|
||||||
|
std::string weightStr = parts[i].substr(7);
|
||||||
|
weight = static_cast<text::FontStyle::Weight>(std::atoi(weightStr.c_str()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text::FontStyle style;
|
const text::FontStyle style(
|
||||||
if (bold && italic)
|
bold ? text::FontStyle::Weight::Bold : weight,
|
||||||
style = text::FontStyle::BoldItalic();
|
text::FontStyle::Width::Normal,
|
||||||
else if (bold)
|
italic ? text::FontStyle::Slant::Italic : text::FontStyle::Slant::Upright);
|
||||||
style = text::FontStyle::Bold();
|
|
||||||
else if (italic)
|
|
||||||
style = text::FontStyle::Italic();
|
|
||||||
|
|
||||||
return app::FontInfo(type, name, size, style, flags, hinting);
|
return app::FontInfo(type, name, size, style, flags, hinting);
|
||||||
}
|
}
|
||||||
|
@ -243,7 +249,7 @@ std::string convert_to(const app::FontInfo& from)
|
||||||
if (!result.empty()) {
|
if (!result.empty()) {
|
||||||
if (from.size() > 0.0f)
|
if (from.size() > 0.0f)
|
||||||
result += fmt::format(",size={}", from.size());
|
result += fmt::format(",size={}", from.size());
|
||||||
if (from.style().weight() >= text::FontStyle::Weight::SemiBold)
|
if (from.style().weight() == text::FontStyle::Weight::Bold)
|
||||||
result += ",bold";
|
result += ",bold";
|
||||||
if (from.style().slant() != text::FontStyle::Slant::Upright)
|
if (from.style().slant() != text::FontStyle::Slant::Upright)
|
||||||
result += ",italic";
|
result += ",italic";
|
||||||
|
@ -262,6 +268,8 @@ std::string convert_to(const app::FontInfo& from)
|
||||||
case text::FontHinting::Full: result += "full"; break;
|
case text::FontHinting::Full: result += "full"; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (from.style().weight() != text::FontStyle::Weight::Bold)
|
||||||
|
result += ",weight=" + std::to_string(static_cast<int>(from.style().weight()));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -681,81 +681,90 @@ void CustomizedGuiManager::onNewDisplayConfiguration(Display* display)
|
||||||
bool CustomizedGuiManager::processKey(Message* msg)
|
bool CustomizedGuiManager::processKey(Message* msg)
|
||||||
{
|
{
|
||||||
App* app = App::instance();
|
App* app = App::instance();
|
||||||
|
const KeyContext currentCtx = KeyboardShortcuts::getCurrentKeyContext();
|
||||||
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
|
||||||
const KeyContext contexts[] = { KeyboardShortcuts::getCurrentKeyContext(), KeyContext::Normal };
|
const KeyContext contexts[] = { currentCtx, KeyContext::Normal };
|
||||||
int n = (contexts[0] != contexts[1] ? 2 : 1);
|
int n = (contexts[0] != contexts[1] ? 2 : 1);
|
||||||
|
|
||||||
|
// Find best match (prefer the shortcut that matches the context first)
|
||||||
|
KeyPtr key = nullptr;
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
for (const KeyPtr& key : *keys) {
|
for (const KeyPtr& k : *keys) {
|
||||||
if (key->isPressed(msg, contexts[i])) {
|
if (k->isPressed(msg, contexts[i]) &&
|
||||||
// Cancel menu-bar loops (to close any popup menu)
|
(!key ||
|
||||||
app->mainWindow()->getMenuBar()->cancelMenuLoop();
|
(key->keycontext() != currentCtx && match_key_context(k->keycontext(), currentCtx)))) {
|
||||||
|
key = k;
|
||||||
switch (key->type()) {
|
|
||||||
case KeyType::Tool: {
|
|
||||||
tools::Tool* current_tool = app->activeTool();
|
|
||||||
tools::Tool* select_this_tool = key->tool();
|
|
||||||
tools::ToolBox* toolbox = app->toolBox();
|
|
||||||
std::vector<tools::Tool*> possibles;
|
|
||||||
|
|
||||||
// Collect all tools with the pressed keyboard-shortcut
|
|
||||||
for (tools::Tool* tool : *toolbox) {
|
|
||||||
const KeyPtr key = keys->tool(tool);
|
|
||||||
if (key && key->isPressed(msg))
|
|
||||||
possibles.push_back(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (possibles.size() >= 2) {
|
|
||||||
bool done = false;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < possibles.size(); ++i) {
|
|
||||||
if (possibles[i] != current_tool &&
|
|
||||||
ToolBar::instance()->isToolVisible(possibles[i])) {
|
|
||||||
select_this_tool = possibles[i];
|
|
||||||
done = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
for (size_t i = 0; i < possibles.size(); ++i) {
|
|
||||||
// If one of the possibilities is the current tool
|
|
||||||
if (possibles[i] == current_tool) {
|
|
||||||
// We select the next tool in the possibilities
|
|
||||||
select_this_tool = possibles[(i + 1) % possibles.size()];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolBar::instance()->selectTool(select_this_tool);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case KeyType::Command: {
|
|
||||||
Command* command = key->command();
|
|
||||||
|
|
||||||
// Commands are executed only when the main window is
|
|
||||||
// the current window running.
|
|
||||||
if (getForegroundWindow() == app->mainWindow()) {
|
|
||||||
// OK, so we can execute the command represented
|
|
||||||
// by the pressed-key in the message...
|
|
||||||
UIContext::instance()->executeCommandFromMenuOrShortcut(command, key->params());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case KeyType::Quicktool: {
|
|
||||||
// Do nothing, it is used in the editor through the
|
|
||||||
// KeyboardShortcuts::getCurrentQuicktool() function.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!key)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Cancel menu-bar loops (to close any popup menu)
|
||||||
|
app->mainWindow()->getMenuBar()->cancelMenuLoop();
|
||||||
|
|
||||||
|
switch (key->type()) {
|
||||||
|
case KeyType::Tool: {
|
||||||
|
tools::Tool* current_tool = app->activeTool();
|
||||||
|
tools::Tool* select_this_tool = key->tool();
|
||||||
|
tools::ToolBox* toolbox = app->toolBox();
|
||||||
|
std::vector<tools::Tool*> possibles;
|
||||||
|
|
||||||
|
// Collect all tools with the pressed keyboard-shortcut
|
||||||
|
for (tools::Tool* tool : *toolbox) {
|
||||||
|
const KeyPtr key = keys->tool(tool);
|
||||||
|
if (key && key->isPressed(msg))
|
||||||
|
possibles.push_back(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (possibles.size() >= 2) {
|
||||||
|
bool done = false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < possibles.size(); ++i) {
|
||||||
|
if (possibles[i] != current_tool && ToolBar::instance()->isToolVisible(possibles[i])) {
|
||||||
|
select_this_tool = possibles[i];
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!done) {
|
||||||
|
for (size_t i = 0; i < possibles.size(); ++i) {
|
||||||
|
// If one of the possibilities is the current tool
|
||||||
|
if (possibles[i] == current_tool) {
|
||||||
|
// We select the next tool in the possibilities
|
||||||
|
select_this_tool = possibles[(i + 1) % possibles.size()];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolBar::instance()->selectTool(select_this_tool);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeyType::Command: {
|
||||||
|
Command* command = key->command();
|
||||||
|
|
||||||
|
// Commands are executed only when the main window is
|
||||||
|
// the current window running.
|
||||||
|
if (getForegroundWindow() == app->mainWindow()) {
|
||||||
|
// OK, so we can execute the command represented
|
||||||
|
// by the pressed-key in the message...
|
||||||
|
UIContext::instance()->executeCommandFromMenuOrShortcut(command, key->params());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeyType::Quicktool: {
|
||||||
|
// Do nothing, it is used in the editor through the
|
||||||
|
// KeyboardShortcuts::getCurrentQuicktool() function.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -213,7 +213,7 @@ void ResourceFinder::includeDesktopDir(const char* filename)
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
std::vector<wchar_t> buf(MAX_PATH);
|
std::vector<wchar_t> buf(MAX_PATH);
|
||||||
HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOPDIRECTORY, NULL, SHGFP_TYPE_DEFAULT, &buf[0]);
|
HRESULT hr = SHGetFolderPath(NULL, CSIDL_DESKTOP, NULL, SHGFP_TYPE_CURRENT, &buf[0]);
|
||||||
if (hr == S_OK) {
|
if (hr == S_OK) {
|
||||||
addPath(base::join_path(base::to_utf8(&buf[0]), filename));
|
addPath(base::join_path(base::to_utf8(&buf[0]), filename));
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@
|
||||||
|
|
||||||
// Increment this value if the scripting API is modified between two
|
// Increment this value if the scripting API is modified between two
|
||||||
// released Aseprite versions.
|
// released Aseprite versions.
|
||||||
#define API_VERSION 34
|
#define API_VERSION 35
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -34,10 +34,10 @@
|
||||||
#include "app/tools/tool_loop_manager.h"
|
#include "app/tools/tool_loop_manager.h"
|
||||||
#include "app/tx.h"
|
#include "app/tx.h"
|
||||||
#include "app/ui/context_bar.h"
|
#include "app/ui/context_bar.h"
|
||||||
#include "app/ui/doc_view.h"
|
|
||||||
#include "app/ui/editor/editor.h"
|
#include "app/ui/editor/editor.h"
|
||||||
#include "app/ui/editor/tool_loop_impl.h"
|
#include "app/ui/editor/tool_loop_impl.h"
|
||||||
#include "app/ui/main_window.h"
|
#include "app/ui/main_window.h"
|
||||||
|
#include "app/ui/status_bar.h"
|
||||||
#include "app/ui/timeline/timeline.h"
|
#include "app/ui/timeline/timeline.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
|
@ -498,6 +498,44 @@ int App_useTool(lua_State* L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int App_tip(lua_State* L)
|
||||||
|
{
|
||||||
|
const auto* ctx = App::instance()->context();
|
||||||
|
if (!ctx || !ctx->isUIAvailable() || !StatusBar::instance())
|
||||||
|
return 0; // No UI to show the tooltip
|
||||||
|
|
||||||
|
std::string text;
|
||||||
|
double duration = 2.0;
|
||||||
|
|
||||||
|
if (lua_istable(L, 1)) {
|
||||||
|
int type = lua_getfield(L, 1, "text");
|
||||||
|
if (type == LUA_TSTRING)
|
||||||
|
text = lua_tostring(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 1, "duration");
|
||||||
|
if (type == LUA_TNUMBER)
|
||||||
|
duration = lua_tonumber(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!lua_isstring(L, 1))
|
||||||
|
return luaL_error(L, "app.tip text parameter must be a string");
|
||||||
|
|
||||||
|
text = lua_tostring(L, 1);
|
||||||
|
|
||||||
|
if (lua_isnumber(L, 2))
|
||||||
|
duration = lua_tonumber(L, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.empty())
|
||||||
|
return luaL_error(L, "app.tip text cannot be empty");
|
||||||
|
|
||||||
|
int msecs = std::clamp<int>(duration * 1000.0, 500, 30000);
|
||||||
|
StatusBar::instance()->showTip(msecs, text);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int App_get_events(lua_State* L)
|
int App_get_events(lua_State* L)
|
||||||
{
|
{
|
||||||
push_app_events(L);
|
push_app_events(L);
|
||||||
|
@ -820,6 +858,7 @@ const luaL_Reg App_methods[] = {
|
||||||
{ "alert", App_alert },
|
{ "alert", App_alert },
|
||||||
{ "refresh", App_refresh },
|
{ "refresh", App_refresh },
|
||||||
{ "useTool", App_useTool },
|
{ "useTool", App_useTool },
|
||||||
|
{ "tip", App_tip },
|
||||||
{ nullptr, nullptr }
|
{ nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,8 @@ namespace app { namespace script {
|
||||||
|
|
||||||
using namespace ui;
|
using namespace ui;
|
||||||
|
|
||||||
|
static constexpr const int kDefaultAutofit = ui::LEFT | ui::TOP;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class DialogWindow : public WindowWithHand {
|
class DialogWindow : public WindowWithHand {
|
||||||
|
@ -107,6 +109,7 @@ struct Dialog {
|
||||||
std::map<std::string, ui::Widget*> dataWidgets;
|
std::map<std::string, ui::Widget*> dataWidgets;
|
||||||
std::map<std::string, ui::Widget*> labelWidgets;
|
std::map<std::string, ui::Widget*> labelWidgets;
|
||||||
int currentRadioGroup = 0;
|
int currentRadioGroup = 0;
|
||||||
|
int autofit = kDefaultAutofit;
|
||||||
|
|
||||||
// Member used to hold current state about the creation of a tabs
|
// Member used to hold current state about the creation of a tabs
|
||||||
// widget. After creation it is reset to null to be ready for the
|
// widget. After creation it is reset to null to be ready for the
|
||||||
|
@ -127,12 +130,13 @@ struct Dialog {
|
||||||
int showRef = LUA_REFNIL;
|
int showRef = LUA_REFNIL;
|
||||||
lua_State* L = nullptr;
|
lua_State* L = nullptr;
|
||||||
|
|
||||||
Dialog(const ui::Window::Type windowType, const std::string& title)
|
Dialog(const ui::Window::Type windowType, const std::string& title, bool sizeable)
|
||||||
: window(windowType, title)
|
: window(windowType, title)
|
||||||
, grid(2, false)
|
, grid(2, false)
|
||||||
, currentGrid(&grid)
|
, currentGrid(&grid)
|
||||||
{
|
{
|
||||||
window.addChild(&grid);
|
window.addChild(&grid);
|
||||||
|
window.setSizeable(sizeable);
|
||||||
all_dialogs.push_back(this);
|
all_dialogs.push_back(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,12 +196,20 @@ struct Dialog {
|
||||||
it->second->setText(text);
|
it->second->setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAutofit(int align)
|
||||||
|
{
|
||||||
|
// Accept both 0 or a valid subset of align parameters.
|
||||||
|
if (align == 0 || (align & (ui::LEFT | ui::RIGHT | ui::TOP | ui::BOTTOM)))
|
||||||
|
autofit = align;
|
||||||
|
}
|
||||||
|
|
||||||
Display* parentDisplay() const
|
Display* parentDisplay() const
|
||||||
{
|
{
|
||||||
Display* parentDisplay = window.parentDisplay();
|
Display* parentDisplay = window.parentDisplay();
|
||||||
if (!parentDisplay) {
|
if (!parentDisplay) {
|
||||||
const auto mainWindow = App::instance()->mainWindow();
|
const auto* mainWindow = App::instance()->mainWindow();
|
||||||
parentDisplay = mainWindow->display();
|
if (mainWindow)
|
||||||
|
parentDisplay = mainWindow->display();
|
||||||
}
|
}
|
||||||
return parentDisplay;
|
return parentDisplay;
|
||||||
}
|
}
|
||||||
|
@ -209,6 +221,9 @@ struct Dialog {
|
||||||
// origin/scale (or main window if a parent window wasn't specified).
|
// origin/scale (or main window if a parent window wasn't specified).
|
||||||
if (window.ownDisplay()) {
|
if (window.ownDisplay()) {
|
||||||
const Display* parentDisplay = this->parentDisplay();
|
const Display* parentDisplay = this->parentDisplay();
|
||||||
|
if (!parentDisplay)
|
||||||
|
return bounds;
|
||||||
|
|
||||||
const int scale = parentDisplay->scale();
|
const int scale = parentDisplay->scale();
|
||||||
const gfx::Point dialogOrigin = window.display()->nativeWindow()->contentRect().origin();
|
const gfx::Point dialogOrigin = window.display()->nativeWindow()->contentRect().origin();
|
||||||
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
||||||
|
@ -223,6 +238,9 @@ struct Dialog {
|
||||||
window.expandWindow(rc.size());
|
window.expandWindow(rc.size());
|
||||||
|
|
||||||
Display* parentDisplay = this->parentDisplay();
|
Display* parentDisplay = this->parentDisplay();
|
||||||
|
if (!parentDisplay)
|
||||||
|
return;
|
||||||
|
|
||||||
const int scale = parentDisplay->scale();
|
const int scale = parentDisplay->scale();
|
||||||
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
|
||||||
gfx::Rect frame = window.display()->nativeWindow()->contentRect();
|
gfx::Rect frame = window.display()->nativeWindow()->contentRect();
|
||||||
|
@ -230,8 +248,10 @@ struct Dialog {
|
||||||
window.display()->nativeWindow()->setFrame(frame);
|
window.display()->nativeWindow()->setFrame(frame);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
gfx::Rect oldBounds(window.bounds());
|
||||||
window.setBounds(rc);
|
window.setBounds(rc);
|
||||||
window.invalidate();
|
window.invalidate();
|
||||||
|
parentDisplay()->invalidateRect(oldBounds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,6 +385,8 @@ int Dialog_new(lua_State* L)
|
||||||
// Get the title and the type of window (with or without title bar)
|
// Get the title and the type of window (with or without title bar)
|
||||||
ui::Window::Type windowType = ui::Window::WithTitleBar;
|
ui::Window::Type windowType = ui::Window::WithTitleBar;
|
||||||
std::string title = "Script";
|
std::string title = "Script";
|
||||||
|
bool sizeable = true;
|
||||||
|
int autofit = kDefaultAutofit;
|
||||||
if (lua_isstring(L, 1)) {
|
if (lua_isstring(L, 1)) {
|
||||||
title = lua_tostring(L, 1);
|
title = lua_tostring(L, 1);
|
||||||
}
|
}
|
||||||
|
@ -378,9 +400,21 @@ int Dialog_new(lua_State* L)
|
||||||
if (type != LUA_TNIL && lua_toboolean(L, -1))
|
if (type != LUA_TNIL && lua_toboolean(L, -1))
|
||||||
windowType = ui::Window::WithoutTitleBar;
|
windowType = ui::Window::WithoutTitleBar;
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 1, "resizeable");
|
||||||
|
if (type != LUA_TNIL && !lua_toboolean(L, -1))
|
||||||
|
sizeable = false;
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 1, "autofit");
|
||||||
|
if (type != LUA_TNIL) {
|
||||||
|
autofit = lua_tointeger(L, -1);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto dlg = push_new<Dialog>(L, windowType, title);
|
auto dlg = push_new<Dialog>(L, windowType, title, sizeable);
|
||||||
|
dlg->setAutofit(autofit);
|
||||||
|
|
||||||
// The uservalue of the dialog userdata will contain a table that
|
// The uservalue of the dialog userdata will contain a table that
|
||||||
// stores all the callbacks to handle events. As these callbacks can
|
// stores all the callbacks to handle events. As these callbacks can
|
||||||
|
@ -1509,6 +1543,10 @@ int Dialog_modify(lua_State* L)
|
||||||
type = lua_getfield(L, 2, "text");
|
type = lua_getfield(L, 2, "text");
|
||||||
if (const char* s = lua_tostring(L, -1)) {
|
if (const char* s = lua_tostring(L, -1)) {
|
||||||
widget->setText(s);
|
widget->setText(s);
|
||||||
|
|
||||||
|
// Re-process mnemonics for buttons
|
||||||
|
if (widget->type() == WidgetType::kButtonWidget)
|
||||||
|
widget->processMnemonicFromText();
|
||||||
relayout = true;
|
relayout = true;
|
||||||
}
|
}
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
@ -1626,6 +1664,10 @@ int Dialog_modify(lua_State* L)
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
type = lua_getfield(L, 2, "mouseCursor");
|
type = lua_getfield(L, 2, "mouseCursor");
|
||||||
|
if (type == LUA_TNIL) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
type = lua_getfield(L, 2, "mousecursor");
|
||||||
|
}
|
||||||
if (type != LUA_TNIL) {
|
if (type != LUA_TNIL) {
|
||||||
if (auto canvas = dynamic_cast<Canvas*>(widget)) {
|
if (auto canvas = dynamic_cast<Canvas*>(widget)) {
|
||||||
auto cursor = (ui::CursorType)lua_tointeger(L, -1);
|
auto cursor = (ui::CursorType)lua_tointeger(L, -1);
|
||||||
|
@ -1643,8 +1685,26 @@ int Dialog_modify(lua_State* L)
|
||||||
if (relayout && !dlg->window.isResizing()) {
|
if (relayout && !dlg->window.isResizing()) {
|
||||||
dlg->window.layout();
|
dlg->window.layout();
|
||||||
|
|
||||||
gfx::Rect bounds(dlg->window.bounds().w, dlg->window.sizeHint().h);
|
if (dlg->autofit > 0) {
|
||||||
dlg->window.expandWindow(bounds.size());
|
gfx::Rect oldBounds = dlg->window.bounds();
|
||||||
|
gfx::Size resize(oldBounds.size());
|
||||||
|
|
||||||
|
if (dlg->autofit & ui::TOP || dlg->autofit & ui::BOTTOM)
|
||||||
|
resize.h = dlg->window.sizeHint().h;
|
||||||
|
if (dlg->autofit & ui::LEFT || dlg->autofit & ui::RIGHT)
|
||||||
|
resize.w = dlg->window.sizeHint().w;
|
||||||
|
|
||||||
|
gfx::Size difference = resize - oldBounds.size();
|
||||||
|
const auto& bounds = dlg->getWindowBounds();
|
||||||
|
gfx::Rect newBounds(bounds.x, bounds.y, resize.w, resize.h);
|
||||||
|
|
||||||
|
if (dlg->autofit & ui::BOTTOM)
|
||||||
|
newBounds.y = bounds.y - difference.h;
|
||||||
|
if (dlg->autofit & ui::RIGHT)
|
||||||
|
newBounds.x = bounds.x - difference.w;
|
||||||
|
|
||||||
|
dlg->setWindowBounds(newBounds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lua_pushvalue(L, 1);
|
lua_pushvalue(L, 1);
|
||||||
|
@ -1866,6 +1926,27 @@ int Dialog_get_bounds(lua_State* L)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Dialog_get_sizeHint(lua_State* L)
|
||||||
|
{
|
||||||
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
push_new<gfx::Size>(L, dlg->window.sizeHint());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Dialog_get_autofit(lua_State* L)
|
||||||
|
{
|
||||||
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
lua_pushinteger(L, dlg->autofit);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Dialog_set_autofit(lua_State* L)
|
||||||
|
{
|
||||||
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
dlg->setAutofit(lua_tointeger(L, 2));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int Dialog_set_bounds(lua_State* L)
|
int Dialog_set_bounds(lua_State* L)
|
||||||
{
|
{
|
||||||
auto dlg = get_obj<Dialog>(L, 1);
|
auto dlg = get_obj<Dialog>(L, 1);
|
||||||
|
@ -1907,9 +1988,11 @@ const luaL_Reg Dialog_methods[] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Property Dialog_properties[] = {
|
const Property Dialog_properties[] = {
|
||||||
{ "data", Dialog_get_data, Dialog_set_data },
|
{ "data", Dialog_get_data, Dialog_set_data },
|
||||||
{ "bounds", Dialog_get_bounds, Dialog_set_bounds },
|
{ "bounds", Dialog_get_bounds, Dialog_set_bounds },
|
||||||
{ nullptr, nullptr, nullptr }
|
{ "autofit", Dialog_get_autofit, Dialog_set_autofit },
|
||||||
|
{ "sizeHint", Dialog_get_sizeHint, nullptr },
|
||||||
|
{ nullptr, nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
|
@ -452,6 +452,7 @@ Engine::Engine() : L(luaL_newstate()), m_delegate(nullptr), m_printLastResult(fa
|
||||||
lua_setglobal(L, "FlipType");
|
lua_setglobal(L, "FlipType");
|
||||||
setfield_integer(L, "HORIZONTAL", doc::algorithm::FlipType::FlipHorizontal);
|
setfield_integer(L, "HORIZONTAL", doc::algorithm::FlipType::FlipHorizontal);
|
||||||
setfield_integer(L, "VERTICAL", doc::algorithm::FlipType::FlipVertical);
|
setfield_integer(L, "VERTICAL", doc::algorithm::FlipType::FlipVertical);
|
||||||
|
setfield_integer(L, "DIAGONAL", doc::algorithm::FlipType::FlipDiagonal);
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
lua_newtable(L);
|
lua_newtable(L);
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include "app/script/luacpp.h"
|
#include "app/script/luacpp.h"
|
||||||
#include "app/script/userdata.h"
|
#include "app/script/userdata.h"
|
||||||
#include "app/tx.h"
|
#include "app/tx.h"
|
||||||
|
#include "app/ui/timeline/timeline.h"
|
||||||
#include "doc/layer.h"
|
#include "doc/layer.h"
|
||||||
#include "doc/layer_tilemap.h"
|
#include "doc/layer_tilemap.h"
|
||||||
#include "doc/sprite.h"
|
#include "doc/sprite.h"
|
||||||
|
@ -355,6 +356,9 @@ int Layer_set_isCollapsed(lua_State* L)
|
||||||
{
|
{
|
||||||
auto layer = get_docobj<Layer>(L, 1);
|
auto layer = get_docobj<Layer>(L, 1);
|
||||||
layer->setCollapsed(lua_toboolean(L, 2));
|
layer->setCollapsed(lua_toboolean(L, 2));
|
||||||
|
|
||||||
|
if (auto* timeline = App::instance()->timeline())
|
||||||
|
timeline->refresh();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,6 +366,9 @@ int Layer_set_isExpanded(lua_State* L)
|
||||||
{
|
{
|
||||||
auto layer = get_docobj<Layer>(L, 1);
|
auto layer = get_docobj<Layer>(L, 1);
|
||||||
layer->setCollapsed(!lua_toboolean(L, 2));
|
layer->setCollapsed(!lua_toboolean(L, 2));
|
||||||
|
|
||||||
|
if (auto* timeline = App::instance()->timeline())
|
||||||
|
timeline->refresh();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,11 +29,16 @@ struct Plugin {
|
||||||
|
|
||||||
class PluginCommand : public Command {
|
class PluginCommand : public Command {
|
||||||
public:
|
public:
|
||||||
PluginCommand(const std::string& id, const std::string& title, int onclickRef, int onenabledRef)
|
PluginCommand(const std::string& id,
|
||||||
|
const std::string& title,
|
||||||
|
int onclickRef,
|
||||||
|
int onenabledRef,
|
||||||
|
int oncheckedRef)
|
||||||
: Command(id.c_str(), CmdUIOnlyFlag)
|
: Command(id.c_str(), CmdUIOnlyFlag)
|
||||||
, m_title(title)
|
, m_title(title)
|
||||||
, m_onclickRef(onclickRef)
|
, m_onclickRef(onclickRef)
|
||||||
, m_onenabledRef(onenabledRef)
|
, m_onenabledRef(onenabledRef)
|
||||||
|
, m_oncheckedRef(oncheckedRef)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,28 +77,42 @@ protected:
|
||||||
bool onEnabled(Context* context) override
|
bool onEnabled(Context* context) override
|
||||||
{
|
{
|
||||||
if (m_onenabledRef) {
|
if (m_onenabledRef) {
|
||||||
script::Engine* engine = App::instance()->scriptEngine();
|
return callScriptRef(m_onenabledRef);
|
||||||
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 true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool onChecked(Context* context) override
|
||||||
|
{
|
||||||
|
if (m_oncheckedRef) {
|
||||||
|
return callScriptRef(m_oncheckedRef);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool callScriptRef(int ref)
|
||||||
|
{
|
||||||
|
ASSERT(ref);
|
||||||
|
script::Engine* engine = App::instance()->scriptEngine();
|
||||||
|
lua_State* L = engine->luaState();
|
||||||
|
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||||
|
if (lua_pcall(L, 0, 1, 0)) {
|
||||||
|
if (const char* s = lua_tostring(L, -1))
|
||||||
|
Console().printf("Error: %s", s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ret = lua_toboolean(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
std::string m_title;
|
std::string m_title;
|
||||||
int m_onclickRef;
|
int m_onclickRef;
|
||||||
int m_onenabledRef;
|
int m_onenabledRef;
|
||||||
|
int m_oncheckedRef;
|
||||||
};
|
};
|
||||||
|
|
||||||
void deleteCommandIfExistent(Extension* ext, const std::string& id)
|
void deleteCommandIfExistent(Extension* ext, const std::string& id)
|
||||||
|
@ -126,6 +145,7 @@ int Plugin_newCommand(lua_State* L)
|
||||||
if (lua_istable(L, 2)) {
|
if (lua_istable(L, 2)) {
|
||||||
std::string id, title, group;
|
std::string id, title, group;
|
||||||
int onenabledRef = 0;
|
int onenabledRef = 0;
|
||||||
|
int oncheckedRef = 0;
|
||||||
|
|
||||||
lua_getfield(L, 2, "id");
|
lua_getfield(L, 2, "id");
|
||||||
if (const char* s = lua_tostring(L, -1)) {
|
if (const char* s = lua_tostring(L, -1)) {
|
||||||
|
@ -156,6 +176,14 @@ int Plugin_newCommand(lua_State* L)
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type = lua_getfield(L, 2, "onchecked");
|
||||||
|
if (type == LUA_TFUNCTION) {
|
||||||
|
oncheckedRef = luaL_ref(L, LUA_REGISTRYINDEX); // does a pop
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
type = lua_getfield(L, 2, "onclick");
|
type = lua_getfield(L, 2, "onclick");
|
||||||
if (type == LUA_TFUNCTION) {
|
if (type == LUA_TFUNCTION) {
|
||||||
int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
@ -164,7 +192,7 @@ int Plugin_newCommand(lua_State* L)
|
||||||
// overwriting a previous registered command)
|
// overwriting a previous registered command)
|
||||||
deleteCommandIfExistent(plugin->ext, id);
|
deleteCommandIfExistent(plugin->ext, id);
|
||||||
|
|
||||||
auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef);
|
auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef, oncheckedRef);
|
||||||
Commands::instance()->add(cmd);
|
Commands::instance()->add(cmd);
|
||||||
plugin->ext->addCommand(id);
|
plugin->ext->addCommand(id);
|
||||||
|
|
||||||
|
@ -172,6 +200,7 @@ int Plugin_newCommand(lua_State* L)
|
||||||
if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
|
if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
|
||||||
if (auto appMenus = AppMenus::instance()) {
|
if (auto appMenus = AppMenus::instance()) {
|
||||||
auto menuItem = std::make_unique<AppMenuItem>(title, id);
|
auto menuItem = std::make_unique<AppMenuItem>(title, id);
|
||||||
|
menuItem->processMnemonicFromText();
|
||||||
appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
|
appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
#include "app/context.h"
|
#include "app/context.h"
|
||||||
#include "app/context_observer.h"
|
#include "app/context_observer.h"
|
||||||
|
#include "app/doc.h"
|
||||||
|
#include "app/doc_undo.h"
|
||||||
#include "app/script/docobj.h"
|
#include "app/script/docobj.h"
|
||||||
#include "app/script/engine.h"
|
#include "app/script/engine.h"
|
||||||
#include "app/script/luacpp.h"
|
#include "app/script/luacpp.h"
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
#include "doc/tag.h"
|
#include "doc/tag.h"
|
||||||
#include "doc/tileset.h"
|
#include "doc/tileset.h"
|
||||||
#include "doc/tilesets.h"
|
#include "doc/tilesets.h"
|
||||||
|
#include "undo/undo_state.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
@ -456,7 +457,7 @@ int Sprite_newCel(lua_State* L)
|
||||||
auto sprite = get_docobj<Sprite>(L, 1);
|
auto sprite = get_docobj<Sprite>(L, 1);
|
||||||
auto layerBase = get_docobj<Layer>(L, 2);
|
auto layerBase = get_docobj<Layer>(L, 2);
|
||||||
if (!layerBase->isImage())
|
if (!layerBase->isImage())
|
||||||
return luaL_error(L, "unexpected kinf of layer in Sprite:newCel()");
|
return luaL_error(L, "unexpected kind of layer in Sprite:newCel()");
|
||||||
|
|
||||||
frame_t frame = get_frame_number_from_arg(L, 3);
|
frame_t frame = get_frame_number_from_arg(L, 3);
|
||||||
if (frame < 0 || frame > sprite->lastFrame())
|
if (frame < 0 || frame > sprite->lastFrame())
|
||||||
|
@ -1029,6 +1030,42 @@ int Sprite_set_useLayerUuids(lua_State* L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Sprite_get_undoHistory(lua_State* L)
|
||||||
|
{
|
||||||
|
const auto* sprite = get_docobj<Sprite>(L, 1);
|
||||||
|
const auto* doc = static_cast<Doc*>(sprite->document());
|
||||||
|
const auto* history = doc->undoHistory();
|
||||||
|
|
||||||
|
if (!history) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const undo::UndoState* currentState = history->currentState();
|
||||||
|
const undo::UndoState* s = history->firstState();
|
||||||
|
const bool canRedo = history->canRedo();
|
||||||
|
bool pastCurrent = !currentState && canRedo;
|
||||||
|
|
||||||
|
int undoSteps = 0;
|
||||||
|
int redoSteps = 0;
|
||||||
|
while (s) {
|
||||||
|
if (pastCurrent && canRedo)
|
||||||
|
redoSteps++;
|
||||||
|
else if (currentState || !canRedo)
|
||||||
|
undoSteps++;
|
||||||
|
|
||||||
|
if (s == currentState || !currentState)
|
||||||
|
pastCurrent = true;
|
||||||
|
|
||||||
|
s = s->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_newtable(L);
|
||||||
|
setfield_integer(L, "undoSteps", undoSteps);
|
||||||
|
setfield_integer(L, "redoSteps", redoSteps);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
const luaL_Reg Sprite_methods[] = {
|
const luaL_Reg Sprite_methods[] = {
|
||||||
{ "__eq", Sprite_eq },
|
{ "__eq", Sprite_eq },
|
||||||
{ "resize", Sprite_resize },
|
{ "resize", Sprite_resize },
|
||||||
|
@ -1094,6 +1131,7 @@ const Property Sprite_properties[] = {
|
||||||
{ "events", Sprite_get_events, nullptr },
|
{ "events", Sprite_get_events, nullptr },
|
||||||
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
|
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
|
||||||
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
|
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
|
||||||
|
{ "undoHistory", Sprite_get_undoHistory, nullptr },
|
||||||
{ nullptr, nullptr, nullptr }
|
{ nullptr, nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
|
@ -1001,12 +1001,12 @@ public:
|
||||||
m_angle.setSuffix("°");
|
m_angle.setSuffix("°");
|
||||||
m_skew.setSuffix("°");
|
m_skew.setSuffix("°");
|
||||||
|
|
||||||
addChild(new Label("P:"));
|
addChild(new Label(Strings::context_bar_position_label()));
|
||||||
addChild(&m_x);
|
addChild(&m_x);
|
||||||
addChild(&m_y);
|
addChild(&m_y);
|
||||||
addChild(&m_w);
|
addChild(&m_w);
|
||||||
addChild(&m_h);
|
addChild(&m_h);
|
||||||
addChild(new Label("R:"));
|
addChild(new Label(Strings::context_bar_rotation_label()));
|
||||||
addChild(&m_angle);
|
addChild(&m_angle);
|
||||||
addChild(&m_skew);
|
addChild(&m_skew);
|
||||||
|
|
||||||
|
@ -1047,6 +1047,16 @@ public:
|
||||||
m_skew.Change.connect([this] { onChangeSkew(); });
|
m_skew.Change.connect([this] { onChangeSkew(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setupTooltips(TooltipManager* tooltipManager)
|
||||||
|
{
|
||||||
|
tooltipManager->addTooltipFor(&m_x, Strings::context_bar_position_x(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_y, Strings::context_bar_position_y(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_w, Strings::context_bar_size_width(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_h, Strings::context_bar_size_height(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_angle, Strings::context_bar_rotation_angle(), BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(&m_skew, Strings::context_bar_rotation_skew(), BOTTOM);
|
||||||
|
}
|
||||||
|
|
||||||
void update(const Transformation& t)
|
void update(const Transformation& t)
|
||||||
{
|
{
|
||||||
auto rc = t.bounds();
|
auto rc = t.bounds();
|
||||||
|
@ -1380,11 +1390,12 @@ public:
|
||||||
|
|
||||||
class ContextBar::DropPixelsField : public ButtonSet {
|
class ContextBar::DropPixelsField : public ButtonSet {
|
||||||
public:
|
public:
|
||||||
DropPixelsField() : ButtonSet(2)
|
DropPixelsField() : ButtonSet(3)
|
||||||
{
|
{
|
||||||
auto* theme = SkinTheme::get(this);
|
auto* theme = SkinTheme::get(this);
|
||||||
|
|
||||||
addItem(theme->parts.dropPixelsOk(), theme->styles.contextBarButton());
|
addItem(theme->parts.dropPixelsOk(), theme->styles.contextBarButton());
|
||||||
|
addItem(theme->parts.dropPixelsDrop(), theme->styles.contextBarButton());
|
||||||
addItem(theme->parts.dropPixelsCancel(), theme->styles.contextBarButton());
|
addItem(theme->parts.dropPixelsCancel(), theme->styles.contextBarButton());
|
||||||
setOfferCapture(false);
|
setOfferCapture(false);
|
||||||
}
|
}
|
||||||
|
@ -1392,8 +1403,26 @@ public:
|
||||||
void setupTooltips(TooltipManager* tooltipManager)
|
void setupTooltips(TooltipManager* tooltipManager)
|
||||||
{
|
{
|
||||||
// TODO Enter and Esc should be configurable keys
|
// TODO Enter and Esc should be configurable keys
|
||||||
tooltipManager->addTooltipFor(at(0), Strings::context_bar_drop_pixel(), BOTTOM);
|
|
||||||
tooltipManager->addTooltipFor(at(1), Strings::context_bar_cancel_drag(), BOTTOM);
|
tooltipManager->addTooltipFor(
|
||||||
|
at(0),
|
||||||
|
key_tooltip(Strings::context_bar_drop_pixel_and_deselect().c_str(),
|
||||||
|
CommandId::DeselectMask(),
|
||||||
|
{},
|
||||||
|
KeyContext::Transformation),
|
||||||
|
BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(at(1),
|
||||||
|
key_tooltip(Strings::context_bar_drop_pixel().c_str(),
|
||||||
|
CommandId::Apply(),
|
||||||
|
{},
|
||||||
|
KeyContext::Transformation),
|
||||||
|
BOTTOM);
|
||||||
|
tooltipManager->addTooltipFor(at(2),
|
||||||
|
key_tooltip(Strings::context_bar_cancel_drag().c_str(),
|
||||||
|
CommandId::Undo(),
|
||||||
|
{},
|
||||||
|
KeyContext::Transformation),
|
||||||
|
BOTTOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
|
obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
|
||||||
|
@ -1404,8 +1433,9 @@ protected:
|
||||||
ButtonSet::onItemChange(item);
|
ButtonSet::onItemChange(item);
|
||||||
|
|
||||||
switch (selectedItem()) {
|
switch (selectedItem()) {
|
||||||
case 0: DropPixels(ContextBarObserver::DropPixels); break;
|
case 0: DropPixels(ContextBarObserver::Deselect); break;
|
||||||
case 1: DropPixels(ContextBarObserver::CancelDrag); break;
|
case 1: DropPixels(ContextBarObserver::DropPixels); break;
|
||||||
|
case 2: DropPixels(ContextBarObserver::CancelDrag); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2626,6 +2656,7 @@ void ContextBar::setupTooltips(TooltipManager* tooltipManager)
|
||||||
m_dropPixels->setupTooltips(tooltipManager);
|
m_dropPixels->setupTooltips(tooltipManager);
|
||||||
m_symmetry->setupTooltips(tooltipManager);
|
m_symmetry->setupTooltips(tooltipManager);
|
||||||
m_sliceFields->setupTooltips(tooltipManager);
|
m_sliceFields->setupTooltips(tooltipManager);
|
||||||
|
m_transformation->setupTooltips(tooltipManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ContextBar::registerCommands()
|
void ContextBar::registerCommands()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2015 David Capello
|
// Copyright (C) 2001-2015 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -12,7 +13,7 @@ namespace app {
|
||||||
|
|
||||||
class ContextBarObserver {
|
class ContextBarObserver {
|
||||||
public:
|
public:
|
||||||
enum DropAction { DropPixels, CancelDrag };
|
enum DropAction { Deselect, DropPixels, CancelDrag };
|
||||||
|
|
||||||
virtual ~ContextBarObserver() {}
|
virtual ~ContextBarObserver() {}
|
||||||
virtual void onDropPixels(DropAction action) {}
|
virtual void onDropPixels(DropAction action) {}
|
||||||
|
|
|
@ -36,12 +36,15 @@
|
||||||
#include "app/ui/workspace.h"
|
#include "app/ui/workspace.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "app/util/clipboard.h"
|
#include "app/util/clipboard.h"
|
||||||
|
#include "app/util/slice_utils.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
#include "doc/color.h"
|
#include "doc/color.h"
|
||||||
#include "doc/layer.h"
|
#include "doc/layer.h"
|
||||||
|
#include "doc/slice.h"
|
||||||
#include "doc/sprite.h"
|
#include "doc/sprite.h"
|
||||||
#include "fmt/format.h"
|
#include "fmt/format.h"
|
||||||
#include "ui/alert.h"
|
#include "ui/alert.h"
|
||||||
|
#include "ui/display.h"
|
||||||
#include "ui/menu.h"
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/shortcut.h"
|
#include "ui/shortcut.h"
|
||||||
|
@ -306,6 +309,11 @@ bool DocView::onCloseView(Workspace* workspace, bool quitting)
|
||||||
|
|
||||||
// See if the sprite has changes
|
// See if the sprite has changes
|
||||||
while (m_document->isModified()) {
|
while (m_document->isModified()) {
|
||||||
|
if (quitting) {
|
||||||
|
// Make sure the window is active so we can see the message when we close the app.
|
||||||
|
display()->nativeWindow()->activate();
|
||||||
|
}
|
||||||
|
|
||||||
// ask what want to do the user with the changes in the sprite
|
// ask what want to do the user with the changes in the sprite
|
||||||
int ret = Alert::show(Strings::alerts_save_sprite_changes(
|
int ret = Alert::show(Strings::alerts_save_sprite_changes(
|
||||||
m_document->name(),
|
m_document->name(),
|
||||||
|
@ -510,6 +518,8 @@ bool DocView::onCanCopy(Context* ctx)
|
||||||
return true;
|
return true;
|
||||||
else if (m_editor->isMovingPixels())
|
else if (m_editor->isMovingPixels())
|
||||||
return true;
|
return true;
|
||||||
|
else if (m_editor->hasSelectedSlices())
|
||||||
|
return true;
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -528,6 +538,11 @@ bool DocView::onCanPaste(Context* ctx)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable) &&
|
||||||
|
ctx->clipboard()->format() == ClipboardFormat::Slices) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,15 +575,22 @@ bool DocView::onCopy(Context* ctx)
|
||||||
ctx->clipboard()->copy(reader);
|
ctx->clipboard()->copy(reader);
|
||||||
return true;
|
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)
|
bool DocView::onPaste(Context* ctx, const gfx::Point* position)
|
||||||
{
|
{
|
||||||
auto clipboard = ctx->clipboard();
|
auto clipboard = ctx->clipboard();
|
||||||
if (clipboard->format() == ClipboardFormat::Image ||
|
if (clipboard->format() == ClipboardFormat::Image ||
|
||||||
clipboard->format() == ClipboardFormat::Tilemap) {
|
clipboard->format() == ClipboardFormat::Tilemap ||
|
||||||
|
clipboard->format() == ClipboardFormat::Slices) {
|
||||||
clipboard->paste(ctx, true, position);
|
clipboard->paste(ctx, true, position);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2555,6 +2555,16 @@ void Editor::onBeforeLayerEditableChange(DocEvent& ev, bool newState)
|
||||||
m_state->onBeforeLayerEditableChange(this, ev.layer(), newState);
|
m_state->onBeforeLayerEditableChange(this, ev.layer(), newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Editor::onBeforeSlicesDuplication(DocEvent& ev)
|
||||||
|
{
|
||||||
|
clearSlicesSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::onSliceDuplicated(DocEvent& ev)
|
||||||
|
{
|
||||||
|
selectSlice(ev.slice());
|
||||||
|
}
|
||||||
|
|
||||||
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
|
void Editor::setCursor(const gfx::Point& mouseDisplayPos)
|
||||||
{
|
{
|
||||||
bool used = false;
|
bool used = false;
|
||||||
|
|
|
@ -343,6 +343,8 @@ protected:
|
||||||
void onRemoveSlice(DocEvent& ev) override;
|
void onRemoveSlice(DocEvent& ev) override;
|
||||||
void onBeforeLayerVisibilityChange(DocEvent& ev, bool newState) override;
|
void onBeforeLayerVisibilityChange(DocEvent& ev, bool newState) override;
|
||||||
void onBeforeLayerEditableChange(DocEvent& ev, bool newState) override;
|
void onBeforeLayerEditableChange(DocEvent& ev, bool newState) override;
|
||||||
|
void onBeforeSlicesDuplication(DocEvent& ev) override;
|
||||||
|
void onSliceDuplicated(DocEvent& ev) override;
|
||||||
|
|
||||||
// ActiveToolObserver impl
|
// ActiveToolObserver impl
|
||||||
void onActiveToolChange(tools::Tool* tool) override;
|
void onActiveToolChange(tools::Tool* tool) override;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -22,6 +22,7 @@
|
||||||
#include "app/commands/commands.h"
|
#include "app/commands/commands.h"
|
||||||
#include "app/commands/move_thing.h"
|
#include "app/commands/move_thing.h"
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/modules/gui.h"
|
#include "app/modules/gui.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/tools/ink.h"
|
#include "app/tools/ink.h"
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
#include "fmt/format.h"
|
#include "fmt/format.h"
|
||||||
#include "gfx/rect.h"
|
#include "gfx/rect.h"
|
||||||
#include "ui/manager.h"
|
#include "ui/manager.h"
|
||||||
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/system.h"
|
#include "ui/system.h"
|
||||||
#include "ui/view.h"
|
#include "ui/view.h"
|
||||||
|
@ -286,9 +288,6 @@ bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
|
||||||
UIContext* ctx = UIContext::instance();
|
UIContext* ctx = UIContext::instance();
|
||||||
ctx->setActiveView(editor->getDocView());
|
ctx->setActiveView(editor->getDocView());
|
||||||
|
|
||||||
ContextBar* contextBar = App::instance()->contextBar();
|
|
||||||
contextBar->updateForMovingPixels(getTransformation(editor));
|
|
||||||
|
|
||||||
// Start scroll loop
|
// Start scroll loop
|
||||||
if (editor->checkForScroll(msg) || editor->checkForZoom(msg))
|
if (editor->checkForScroll(msg) || editor->checkForZoom(msg))
|
||||||
return true;
|
return true;
|
||||||
|
@ -442,10 +441,6 @@ void MovingPixelsState::onCommitMouseMove(Editor* editor, const gfx::PointF& spr
|
||||||
// Drag the image to that position
|
// Drag the image to that position
|
||||||
m_pixelsMovement->moveImage(spritePos, moveModifier);
|
m_pixelsMovement->moveImage(spritePos, moveModifier);
|
||||||
|
|
||||||
// Update context bar and status bar
|
|
||||||
ContextBar* contextBar = App::instance()->contextBar();
|
|
||||||
contextBar->updateForMovingPixels(transformation);
|
|
||||||
|
|
||||||
m_editor->updateStatusBar();
|
m_editor->updateStatusBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,19 +470,6 @@ bool MovingPixelsState::onKeyDown(Editor* editor, KeyMessage* msg)
|
||||||
// FineControl now (e.g. if we pressed another modifier key).
|
// FineControl now (e.g. if we pressed another modifier key).
|
||||||
m_lockedKeyAction = KeyAction::None;
|
m_lockedKeyAction = KeyAction::None;
|
||||||
|
|
||||||
if (msg->scancode() == kKeyEnter || // TODO make this key customizable
|
|
||||||
msg->scancode() == kKeyEnterPad || msg->scancode() == kKeyEsc) {
|
|
||||||
dropPixels();
|
|
||||||
|
|
||||||
// The escape key drop pixels and deselect the mask.
|
|
||||||
if (msg->scancode() == kKeyEsc) { // TODO make this key customizable
|
|
||||||
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
|
|
||||||
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use StandbyState implementation
|
// Use StandbyState implementation
|
||||||
return StandbyState::onKeyDown(editor, msg);
|
return StandbyState::onKeyDown(editor, msg);
|
||||||
}
|
}
|
||||||
|
@ -529,6 +511,10 @@ bool MovingPixelsState::onUpdateStatusBar(Editor* editor)
|
||||||
const Transformation& transform(getTransformation(editor));
|
const Transformation& transform(getTransformation(editor));
|
||||||
gfx::Size imageSize = m_pixelsMovement->getInitialImageSize();
|
gfx::Size imageSize = m_pixelsMovement->getInitialImageSize();
|
||||||
|
|
||||||
|
// Update the context bar along with the status bar
|
||||||
|
ContextBar* contextBar = App::instance()->contextBar();
|
||||||
|
contextBar->updateForMovingPixels(transform);
|
||||||
|
|
||||||
int w = int(transform.bounds().w);
|
int w = int(transform.bounds().w);
|
||||||
int h = int(transform.bounds().h);
|
int h = int(transform.bounds().h);
|
||||||
int gcd = base::gcd(w, h);
|
int gcd = base::gcd(w, h);
|
||||||
|
@ -634,6 +620,12 @@ void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Handle undo directly as cancelDrag() to avoid adding an action in the history.
|
||||||
|
else if (command->id() == CommandId::Undo()) {
|
||||||
|
cancelDrag();
|
||||||
|
ev.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Don't drop pixels if the user zooms/scrolls/picks a color
|
// Don't drop pixels if the user zooms/scrolls/picks a color
|
||||||
// using commands.
|
// using commands.
|
||||||
else if ((command->id() == CommandId::Zoom()) || (command->id() == CommandId::Scroll()) ||
|
else if ((command->id() == CommandId::Zoom()) || (command->id() == CommandId::Scroll()) ||
|
||||||
|
@ -795,18 +787,35 @@ void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case ContextBarObserver::Deselect: deselect(); break;
|
||||||
case ContextBarObserver::DropPixels: dropPixels(); break;
|
case ContextBarObserver::DropPixels: dropPixels(); break;
|
||||||
|
case ContextBarObserver::CancelDrag: cancelDrag(); break;
|
||||||
case ContextBarObserver::CancelDrag:
|
|
||||||
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
|
|
||||||
m_discarded = true;
|
|
||||||
|
|
||||||
// Quit from MovingPixelsState, back to standby.
|
|
||||||
m_editor->backToPreviousState();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MovingPixelsState::deselect()
|
||||||
|
{
|
||||||
|
if (!m_pixelsMovement || m_discarded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
dropPixels();
|
||||||
|
|
||||||
|
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
|
||||||
|
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MovingPixelsState::cancelDrag()
|
||||||
|
{
|
||||||
|
if (!m_pixelsMovement || m_discarded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
|
||||||
|
m_discarded = true;
|
||||||
|
|
||||||
|
// Quit from MovingPixelsState, back to standby.
|
||||||
|
dropPixels();
|
||||||
|
}
|
||||||
|
|
||||||
void MovingPixelsState::onPivotChange()
|
void MovingPixelsState::onPivotChange()
|
||||||
{
|
{
|
||||||
ContextBar* contextBar = App::instance()->contextBar();
|
ContextBar* contextBar = App::instance()->contextBar();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -94,7 +94,10 @@ private:
|
||||||
void onBeforeCommandExecution(CommandExecutionEvent& ev);
|
void onBeforeCommandExecution(CommandExecutionEvent& ev);
|
||||||
|
|
||||||
void setTransparentColor(bool opaque, const app::Color& color);
|
void setTransparentColor(bool opaque, const app::Color& color);
|
||||||
|
|
||||||
|
void deselect();
|
||||||
void dropPixels();
|
void dropPixels();
|
||||||
|
void cancelDrag();
|
||||||
|
|
||||||
bool isActiveDocument() const;
|
bool isActiveDocument() const;
|
||||||
bool isActiveEditor() const;
|
bool isActiveEditor() const;
|
||||||
|
|
|
@ -74,7 +74,11 @@ bool StateWithWheelBehavior::onMouseWheel(Editor* editor, MouseMessage* msg)
|
||||||
double dz = delta.x + delta.y;
|
double dz = delta.x + delta.y;
|
||||||
WheelAction wheelAction = WheelAction::None;
|
WheelAction wheelAction = WheelAction::None;
|
||||||
|
|
||||||
if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
|
if (tools::Tool* quickTool = App::instance()->activeToolManager()->quickTool();
|
||||||
|
quickTool && quickTool->getId() == tools::WellKnownInks::Zoom) {
|
||||||
|
wheelAction = WheelAction::Zoom;
|
||||||
|
}
|
||||||
|
else if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
|
||||||
if (!Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel())
|
if (!Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel())
|
||||||
wheelAction = WheelAction::VScroll;
|
wheelAction = WheelAction::VScroll;
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -45,6 +45,7 @@
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "app/util/expand_cel_canvas.h"
|
#include "app/util/expand_cel_canvas.h"
|
||||||
#include "app/util/layer_utils.h"
|
#include "app/util/layer_utils.h"
|
||||||
|
#include "app/util/slice_utils.h"
|
||||||
#include "doc/brush.h"
|
#include "doc/brush.h"
|
||||||
#include "doc/cel.h"
|
#include "doc/cel.h"
|
||||||
#include "doc/image.h"
|
#include "doc/image.h"
|
||||||
|
@ -693,7 +694,7 @@ public:
|
||||||
// popup menu to create a new one.
|
// popup menu to create a new one.
|
||||||
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
|
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
|
||||||
Slice* slice = new Slice;
|
Slice* slice = new Slice;
|
||||||
slice->setName(getUniqueSliceName());
|
slice->setName(get_unique_slice_name(m_sprite));
|
||||||
|
|
||||||
SliceKey key(bounds);
|
SliceKey key(bounds);
|
||||||
slice->insert(getFrame(), key);
|
slice->insert(getFrame(), key);
|
||||||
|
@ -716,18 +717,6 @@ private:
|
||||||
// EditorObserver impl
|
// EditorObserver impl
|
||||||
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||||
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||||
|
|
||||||
std::string getUniqueSliceName() const
|
|
||||||
{
|
|
||||||
std::string prefix = "Slice";
|
|
||||||
int max = 0;
|
|
||||||
|
|
||||||
for (Slice* slice : m_sprite->slices())
|
|
||||||
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
|
|
||||||
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
|
|
||||||
|
|
||||||
return fmt::format("{} {}", prefix, max + 1);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2020-2022 Igara Studio S.A.
|
// Copyright (C) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -209,7 +209,8 @@ gfx::Rect TransformHandles::getPivotHandleBounds(Editor* editor,
|
||||||
{
|
{
|
||||||
auto theme = SkinTheme::get(editor);
|
auto theme = SkinTheme::get(editor);
|
||||||
gfx::Size partSize = theme->parts.pivotHandle()->size();
|
gfx::Size partSize = theme->parts.pivotHandle()->size();
|
||||||
gfx::Point screenPivotPos = editor->editorToScreen(gfx::Point(transform.pivot()));
|
gfx::Point pivotPos = gfx::Point(transform.pivot()) + editor->mainTilePosition();
|
||||||
|
gfx::Point screenPivotPos = editor->editorToScreen(pivotPos);
|
||||||
|
|
||||||
screenPivotPos.x += editor->projection().applyX(1) / 2;
|
screenPivotPos.x += editor->projection().applyX(1) / 2;
|
||||||
screenPivotPos.y += editor->projection().applyY(1) / 2;
|
screenPivotPos.y += editor->projection().applyY(1) / 2;
|
||||||
|
|
|
@ -443,12 +443,7 @@ bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WritingTextState::onKeyDown(Editor*, KeyMessage*)
|
bool WritingTextState::onKeyDown(Editor*, KeyMessage* msg)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
|
||||||
{
|
{
|
||||||
// Cancel loop pressing Esc key
|
// Cancel loop pressing Esc key
|
||||||
if (msg->scancode() == ui::kKeyEsc) {
|
if (msg->scancode() == ui::kKeyEsc) {
|
||||||
|
@ -457,7 +452,17 @@ bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
||||||
// Drop text pressing Enter key
|
// Drop text pressing Enter key
|
||||||
else if (msg->scancode() == ui::kKeyEnter) {
|
else if (msg->scancode() == ui::kKeyEnter) {
|
||||||
drop();
|
drop();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
|
||||||
|
{
|
||||||
|
// Note: We cannot process kKeyEnter key here to drop the text as it
|
||||||
|
// could be received after the Enter key is pressed in the IME
|
||||||
|
// dialog to accept the composition (not to accept the text). So we
|
||||||
|
// process kKeyEnter in onKeyDown().
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,11 @@
|
||||||
#include "base/convert_to.h"
|
#include "base/convert_to.h"
|
||||||
#include "base/scoped_value.h"
|
#include "base/scoped_value.h"
|
||||||
#include "fmt/format.h"
|
#include "fmt/format.h"
|
||||||
|
#include "text/font_style_set.h"
|
||||||
#include "ui/display.h"
|
#include "ui/display.h"
|
||||||
#include "ui/fit_bounds.h"
|
#include "ui/fit_bounds.h"
|
||||||
#include "ui/manager.h"
|
#include "ui/manager.h"
|
||||||
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/scale.h"
|
#include "ui/scale.h"
|
||||||
|
|
||||||
|
@ -271,7 +273,7 @@ FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, tru
|
||||||
addItem("...");
|
addItem("...");
|
||||||
setMultiMode(MultiMode::Set);
|
setMultiMode(MultiMode::Set);
|
||||||
|
|
||||||
tooltips->addTooltipFor(getItem(0), Strings::text_tool_bold(), BOTTOM);
|
tooltips->addTooltipFor(getItem(0), Strings::font_style_font_weight(), BOTTOM);
|
||||||
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
|
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
|
||||||
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
|
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
|
||||||
}
|
}
|
||||||
|
@ -384,17 +386,72 @@ void FontEntry::setInfo(const FontInfo& info, const From fromField)
|
||||||
{
|
{
|
||||||
m_info = info;
|
m_info = info;
|
||||||
|
|
||||||
if (fromField != From::Face)
|
auto family = theme()->fontMgr()->matchFamily(m_info.name());
|
||||||
m_face.setText(info.title());
|
bool hasBold = false;
|
||||||
|
m_availableWeights.clear();
|
||||||
|
|
||||||
|
if (family) {
|
||||||
|
auto checkWeight = [this, &family, &hasBold](text::FontStyle::Weight w) {
|
||||||
|
auto ref = family->matchStyle(
|
||||||
|
text::FontStyle(w, m_info.style().width(), m_info.style().slant()));
|
||||||
|
if (ref->fontStyle().weight() == w)
|
||||||
|
m_availableWeights.push_back(w);
|
||||||
|
|
||||||
|
if (ref->fontStyle().weight() == text::FontStyle::Weight::Bold)
|
||||||
|
hasBold = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkWeight(text::FontStyle::Weight::Thin);
|
||||||
|
checkWeight(text::FontStyle::Weight::ExtraLight);
|
||||||
|
checkWeight(text::FontStyle::Weight::Light);
|
||||||
|
checkWeight(text::FontStyle::Weight::Normal);
|
||||||
|
checkWeight(text::FontStyle::Weight::Medium);
|
||||||
|
checkWeight(text::FontStyle::Weight::SemiBold);
|
||||||
|
checkWeight(text::FontStyle::Weight::Bold);
|
||||||
|
checkWeight(text::FontStyle::Weight::Black);
|
||||||
|
checkWeight(text::FontStyle::Weight::ExtraBlack);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Stick to only "normal" for fonts without a family.
|
||||||
|
m_availableWeights.push_back(text::FontStyle::Weight::Normal);
|
||||||
|
m_style.getItem(0)->setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::find(m_availableWeights.begin(), m_availableWeights.end(), m_info.style().weight()) ==
|
||||||
|
m_availableWeights.end()) {
|
||||||
|
// The currently selected weight is not available, reset it back to normal.
|
||||||
|
m_info = app::FontInfo(m_info,
|
||||||
|
m_info.size(),
|
||||||
|
text::FontStyle(text::FontStyle::Weight::Normal,
|
||||||
|
m_info.style().width(),
|
||||||
|
m_info.style().slant()),
|
||||||
|
m_info.flags(),
|
||||||
|
m_info.hinting());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromField != From::Face) {
|
||||||
|
m_face.setText(m_info.title());
|
||||||
|
}
|
||||||
|
|
||||||
if (fromField != From::Size) {
|
if (fromField != From::Size) {
|
||||||
m_size.updateForFont(info);
|
m_size.updateForFont(m_info);
|
||||||
m_size.setValue(fmt::format("{}", info.size()));
|
m_size.setValue(fmt::format("{}", m_info.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_style.getItem(0)->setEnabled(hasBold);
|
||||||
|
m_style.getItem(0)->setSelected(m_info.style().weight() != text::FontStyle::Weight::Normal);
|
||||||
|
m_style.getItem(0)->setText("B");
|
||||||
|
|
||||||
|
// Give some indication of what the weight is, if we have any variation
|
||||||
|
if (m_style.getItem(0)->isSelected() && m_availableWeights.size() > 1) {
|
||||||
|
if (m_info.style().weight() > text::FontStyle::Weight::Bold)
|
||||||
|
m_style.getItem(0)->setText("B+");
|
||||||
|
else if (m_info.style().weight() < text::FontStyle::Weight::Bold)
|
||||||
|
m_style.getItem(0)->setText("B-");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fromField != From::Style) {
|
if (fromField != From::Style) {
|
||||||
m_style.getItem(0)->setSelected(info.style().weight() >= text::FontStyle::Weight::SemiBold);
|
m_style.getItem(1)->setSelected(m_info.style().slant() != text::FontStyle::Slant::Upright);
|
||||||
m_style.getItem(1)->setSelected(info.style().slant() != text::FontStyle::Slant::Upright);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FontChange(m_info, fromField);
|
FontChange(m_info, fromField);
|
||||||
|
@ -430,17 +487,51 @@ void FontEntry::onStyleItemClick(ButtonSet::Item* item)
|
||||||
switch (m_style.getItemIndex(item)) {
|
switch (m_style.getItemIndex(item)) {
|
||||||
// Bold button changed
|
// Bold button changed
|
||||||
case 0: {
|
case 0: {
|
||||||
const bool bold = m_style.getItem(0)->isSelected();
|
if (m_availableWeights.size() > 2) {
|
||||||
style = text::FontStyle(
|
// Ensure consistency, since the click also affects the "selected" highlighting.
|
||||||
bold ? text::FontStyle::Weight::Bold : text::FontStyle::Weight::Normal,
|
item->setSelected(style.weight() != text::FontStyle::Weight::Normal);
|
||||||
style.width(),
|
|
||||||
style.slant());
|
|
||||||
|
|
||||||
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
|
Menu weightMenu;
|
||||||
From::Style);
|
auto currentWeight = m_info.style().weight();
|
||||||
|
|
||||||
|
auto weightChange = [this](text::FontStyle::Weight newWeight) {
|
||||||
|
const text::FontStyle style(newWeight, m_info.style().width(), m_info.style().slant());
|
||||||
|
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
|
||||||
|
From::Style);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto weight : m_availableWeights) {
|
||||||
|
auto* menuItem = new MenuItem(Strings::Translate(
|
||||||
|
fmt::format("font_style.font_weight_{}", static_cast<int>(weight)).c_str()));
|
||||||
|
menuItem->setSelected(weight == currentWeight);
|
||||||
|
if (!menuItem->isSelected())
|
||||||
|
menuItem->Click.connect([&weightChange, weight] { weightChange(weight); });
|
||||||
|
|
||||||
|
if (weight == text::FontStyle::Weight::Bold &&
|
||||||
|
currentWeight == text::FontStyle::Weight::Normal)
|
||||||
|
menuItem->setHighlighted(true);
|
||||||
|
|
||||||
|
weightMenu.addChild(menuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
weightMenu.initTheme();
|
||||||
|
const auto& bounds = m_style.getItem(0)->bounds();
|
||||||
|
|
||||||
|
weightMenu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const bool isBold = m_info.style().weight() == text::FontStyle::Weight::Bold;
|
||||||
|
style = text::FontStyle(
|
||||||
|
isBold ? text::FontStyle::Weight::Normal : text::FontStyle::Weight::Bold,
|
||||||
|
style.width(),
|
||||||
|
style.slant());
|
||||||
|
|
||||||
|
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
|
||||||
|
From::Style);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Italic button changed
|
// Italic button changed
|
||||||
case 1: {
|
case 1: {
|
||||||
const bool italic = m_style.getItem(1)->isSelected();
|
const bool italic = m_style.getItem(1)->isSelected();
|
||||||
style = text::FontStyle(
|
style = text::FontStyle(
|
||||||
|
|
|
@ -115,6 +115,7 @@ private:
|
||||||
FontSize m_size;
|
FontSize m_size;
|
||||||
FontStyle m_style;
|
FontStyle m_style;
|
||||||
std::unique_ptr<FontStroke> m_stroke;
|
std::unique_ptr<FontStroke> m_stroke;
|
||||||
|
std::vector<text::FontStyle::Weight> m_availableWeights;
|
||||||
bool m_lockFace = false;
|
bool m_lockFace = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,593 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||||
|
// Copyright (C) 2001-2018 David Capello
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "app/ui/key.h"
|
||||||
|
|
||||||
|
#include "app/commands/command.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
|
#include "app/tools/tool.h"
|
||||||
|
#include "app/ui/keyboard_shortcuts.h"
|
||||||
|
#include "ui/message.h"
|
||||||
|
#include "ui/shortcut.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define I18N_KEY(a) app::Strings::keyboard_shortcuts_##a()
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct KeyShortcutAction {
|
||||||
|
const char* name;
|
||||||
|
std::string userfriendly;
|
||||||
|
app::KeyAction action;
|
||||||
|
app::KeyContext context;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<KeyShortcutAction> g_actions;
|
||||||
|
|
||||||
|
const std::vector<KeyShortcutAction>& actions()
|
||||||
|
{
|
||||||
|
if (g_actions.empty()) {
|
||||||
|
g_actions = std::vector<KeyShortcutAction>{
|
||||||
|
{ "CopySelection",
|
||||||
|
I18N_KEY(copy_selection),
|
||||||
|
app::KeyAction::CopySelection,
|
||||||
|
app::KeyContext::TranslatingSelection },
|
||||||
|
{ "SnapToGrid",
|
||||||
|
I18N_KEY(snap_to_grid),
|
||||||
|
app::KeyAction::SnapToGrid,
|
||||||
|
app::KeyContext::TranslatingSelection },
|
||||||
|
{ "LockAxis",
|
||||||
|
I18N_KEY(lock_axis),
|
||||||
|
app::KeyAction::LockAxis,
|
||||||
|
app::KeyContext::TranslatingSelection },
|
||||||
|
{ "FineControl",
|
||||||
|
I18N_KEY(fine_translating),
|
||||||
|
app::KeyAction::FineControl,
|
||||||
|
app::KeyContext::TranslatingSelection },
|
||||||
|
{ "MaintainAspectRatio",
|
||||||
|
I18N_KEY(maintain_aspect_ratio),
|
||||||
|
app::KeyAction::MaintainAspectRatio,
|
||||||
|
app::KeyContext::ScalingSelection },
|
||||||
|
{ "ScaleFromCenter",
|
||||||
|
I18N_KEY(scale_from_center),
|
||||||
|
app::KeyAction::ScaleFromCenter,
|
||||||
|
app::KeyContext::ScalingSelection },
|
||||||
|
{ "FineControl",
|
||||||
|
I18N_KEY(fine_scaling),
|
||||||
|
app::KeyAction::FineControl,
|
||||||
|
app::KeyContext::ScalingSelection },
|
||||||
|
{ "AngleSnap",
|
||||||
|
I18N_KEY(angle_snap),
|
||||||
|
app::KeyAction::AngleSnap,
|
||||||
|
app::KeyContext::RotatingSelection },
|
||||||
|
{ "AddSelection",
|
||||||
|
I18N_KEY(add_selection),
|
||||||
|
app::KeyAction::AddSelection,
|
||||||
|
app::KeyContext::SelectionTool },
|
||||||
|
{ "SubtractSelection",
|
||||||
|
I18N_KEY(subtract_selection),
|
||||||
|
app::KeyAction::SubtractSelection,
|
||||||
|
app::KeyContext::SelectionTool },
|
||||||
|
{ "IntersectSelection",
|
||||||
|
I18N_KEY(intersect_selection),
|
||||||
|
app::KeyAction::IntersectSelection,
|
||||||
|
app::KeyContext::SelectionTool },
|
||||||
|
{ "AutoSelectLayer",
|
||||||
|
I18N_KEY(auto_select_layer),
|
||||||
|
app::KeyAction::AutoSelectLayer,
|
||||||
|
app::KeyContext::MoveTool },
|
||||||
|
{ "StraightLineFromLastPoint",
|
||||||
|
I18N_KEY(line_from_last_point),
|
||||||
|
app::KeyAction::StraightLineFromLastPoint,
|
||||||
|
app::KeyContext::FreehandTool },
|
||||||
|
{ "AngleSnapFromLastPoint",
|
||||||
|
I18N_KEY(angle_from_last_point),
|
||||||
|
app::KeyAction::AngleSnapFromLastPoint,
|
||||||
|
app::KeyContext::FreehandTool },
|
||||||
|
{ "MoveOrigin",
|
||||||
|
I18N_KEY(move_origin),
|
||||||
|
app::KeyAction::MoveOrigin,
|
||||||
|
app::KeyContext::ShapeTool },
|
||||||
|
{ "SquareAspect",
|
||||||
|
I18N_KEY(square_aspect),
|
||||||
|
app::KeyAction::SquareAspect,
|
||||||
|
app::KeyContext::ShapeTool },
|
||||||
|
{ "DrawFromCenter",
|
||||||
|
I18N_KEY(draw_from_center),
|
||||||
|
app::KeyAction::DrawFromCenter,
|
||||||
|
app::KeyContext::ShapeTool },
|
||||||
|
{ "RotateShape",
|
||||||
|
I18N_KEY(rotate_shape),
|
||||||
|
app::KeyAction::RotateShape,
|
||||||
|
app::KeyContext::ShapeTool },
|
||||||
|
{ "LeftMouseButton",
|
||||||
|
I18N_KEY(trigger_left_mouse_button),
|
||||||
|
app::KeyAction::LeftMouseButton,
|
||||||
|
app::KeyContext::Any },
|
||||||
|
{ "RightMouseButton",
|
||||||
|
I18N_KEY(trigger_right_mouse_button),
|
||||||
|
app::KeyAction::RightMouseButton,
|
||||||
|
app::KeyContext::Any }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return g_actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct {
|
||||||
|
const char* name;
|
||||||
|
app::KeyContext context;
|
||||||
|
} g_contexts[] = {
|
||||||
|
{ "", app::KeyContext::Any },
|
||||||
|
{ "Normal", app::KeyContext::Normal },
|
||||||
|
{ "Selection", app::KeyContext::SelectionTool },
|
||||||
|
{ "TranslatingSelection", app::KeyContext::TranslatingSelection },
|
||||||
|
{ "ScalingSelection", app::KeyContext::ScalingSelection },
|
||||||
|
{ "RotatingSelection", app::KeyContext::RotatingSelection },
|
||||||
|
{ "MoveTool", app::KeyContext::MoveTool },
|
||||||
|
{ "FreehandTool", app::KeyContext::FreehandTool },
|
||||||
|
{ "ShapeTool", app::KeyContext::ShapeTool },
|
||||||
|
{ "FramesSelection", app::KeyContext::FramesSelection },
|
||||||
|
{ "Transformation", app::KeyContext::Transformation },
|
||||||
|
{ NULL, app::KeyContext::Any }
|
||||||
|
};
|
||||||
|
|
||||||
|
using Vec = app::DragVector;
|
||||||
|
|
||||||
|
struct KeyShortcutWheelAction {
|
||||||
|
const char* name;
|
||||||
|
const std::string userfriendly;
|
||||||
|
Vec vector;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<KeyShortcutWheelAction> g_wheel_actions;
|
||||||
|
|
||||||
|
const std::vector<KeyShortcutWheelAction>& wheel_actions()
|
||||||
|
{
|
||||||
|
if (g_wheel_actions.empty()) {
|
||||||
|
g_wheel_actions = std::vector<KeyShortcutWheelAction>{
|
||||||
|
{ "", "", Vec(0.0, 0.0) },
|
||||||
|
{ "Zoom", I18N_KEY(zoom), Vec(8.0, 0.0) },
|
||||||
|
{ "VScroll", I18N_KEY(scroll_vertically), Vec(4.0, 0.0) },
|
||||||
|
{ "HScroll", I18N_KEY(scroll_horizontally), Vec(4.0, 0.0) },
|
||||||
|
{ "FgColor", I18N_KEY(fg_color), Vec(8.0, 0.0) },
|
||||||
|
{ "BgColor", I18N_KEY(bg_color), Vec(8.0, 0.0) },
|
||||||
|
{ "FgTile", I18N_KEY(fg_tile), Vec(8.0, 0.0) },
|
||||||
|
{ "BgTile", I18N_KEY(bg_tile), Vec(8.0, 0.0) },
|
||||||
|
{ "Frame", I18N_KEY(change_frame), Vec(16.0, 0.0) },
|
||||||
|
{ "BrushSize", I18N_KEY(change_brush_size), Vec(4.0, 0.0) },
|
||||||
|
{ "BrushAngle", I18N_KEY(change_brush_angle), Vec(-4.0, 0.0) },
|
||||||
|
{ "ToolSameGroup", I18N_KEY(change_tool_same_group), Vec(8.0, 0.0) },
|
||||||
|
{ "ToolOtherGroup", I18N_KEY(change_tool), Vec(0.0, -8.0) },
|
||||||
|
{ "Layer", I18N_KEY(change_layer), Vec(0.0, 8.0) },
|
||||||
|
{ "InkType", I18N_KEY(change_ink_type), Vec(0.0, -16.0) },
|
||||||
|
{ "InkOpacity", I18N_KEY(change_ink_opacity), Vec(0.0, 1.0) },
|
||||||
|
{ "LayerOpacity", I18N_KEY(change_layer_opacity), Vec(0.0, 1.0) },
|
||||||
|
{ "CelOpacity", I18N_KEY(change_cel_opacity), Vec(0.0, 1.0) },
|
||||||
|
{ "Alpha", I18N_KEY(color_alpha), Vec(4.0, 0.0) },
|
||||||
|
{ "HslHue", I18N_KEY(color_hsl_hue), Vec(1.0, 0.0) },
|
||||||
|
{ "HslSaturation", I18N_KEY(color_hsl_saturation), Vec(4.0, 0.0) },
|
||||||
|
{ "HslLightness", I18N_KEY(color_hsl_lightness), Vec(0.0, 4.0) },
|
||||||
|
{ "HsvHue", I18N_KEY(color_hsv_hue), Vec(1.0, 0.0) },
|
||||||
|
{ "HsvSaturation", I18N_KEY(color_hsv_saturation), Vec(4.0, 0.0) },
|
||||||
|
{ "HsvValue", I18N_KEY(color_hsv_value), Vec(0.0, 4.0) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return g_wheel_actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get_user_friendly_string_for_keyaction(app::KeyAction action, app::KeyContext context)
|
||||||
|
{
|
||||||
|
for (const auto& a : actions()) {
|
||||||
|
if (action == a.action && context == a.context)
|
||||||
|
return a.userfriendly;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelAction)
|
||||||
|
{
|
||||||
|
const int c = int(wheelAction);
|
||||||
|
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last))
|
||||||
|
return wheel_actions()[c].userfriendly;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void erase_shortcut(app::KeySourceShortcutList& kvs,
|
||||||
|
const app::KeySource source,
|
||||||
|
const ui::Shortcut& shortcut)
|
||||||
|
{
|
||||||
|
for (auto it = kvs.begin(); it != kvs.end();) {
|
||||||
|
auto& kv = *it;
|
||||||
|
if (kv.first == source && kv.second == shortcut) {
|
||||||
|
it = kvs.erase(it);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void erase_shortcuts(app::KeySourceShortcutList& kvs, const app::KeySource source)
|
||||||
|
{
|
||||||
|
for (auto it = kvs.begin(); it != kvs.end();) {
|
||||||
|
auto& kv = *it;
|
||||||
|
if (kv.first == source) {
|
||||||
|
it = kvs.erase(it);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
namespace base {
|
||||||
|
|
||||||
|
template<>
|
||||||
|
app::KeyAction convert_to(const std::string& from)
|
||||||
|
{
|
||||||
|
for (const auto& a : actions()) {
|
||||||
|
if (from == a.name)
|
||||||
|
return a.action;
|
||||||
|
}
|
||||||
|
return app::KeyAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
std::string convert_to(const app::KeyAction& from)
|
||||||
|
{
|
||||||
|
for (const auto& a : actions()) {
|
||||||
|
if (from == a.action)
|
||||||
|
return a.name;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
app::WheelAction convert_to(const std::string& from)
|
||||||
|
{
|
||||||
|
for (int c = int(app::WheelAction::First); c <= int(app::WheelAction::Last); ++c) {
|
||||||
|
if (from == wheel_actions()[c].name)
|
||||||
|
return (app::WheelAction)c;
|
||||||
|
}
|
||||||
|
return app::WheelAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
std::string convert_to(const app::WheelAction& from)
|
||||||
|
{
|
||||||
|
const int c = int(from);
|
||||||
|
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last))
|
||||||
|
return wheel_actions()[c].name;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
app::KeyContext convert_to(const std::string& from)
|
||||||
|
{
|
||||||
|
for (int c = 0; g_contexts[c].name; ++c) {
|
||||||
|
if (from == g_contexts[c].name)
|
||||||
|
return g_contexts[c].context;
|
||||||
|
}
|
||||||
|
return app::KeyContext::Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
std::string convert_to(const app::KeyContext& from)
|
||||||
|
{
|
||||||
|
for (int c = 0; g_contexts[c].name; ++c) {
|
||||||
|
if (from == g_contexts[c].context)
|
||||||
|
return g_contexts[c].name;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace base
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
using namespace ui;
|
||||||
|
|
||||||
|
Key::Key(const Key& k)
|
||||||
|
: m_type(k.m_type)
|
||||||
|
, m_adds(k.m_adds)
|
||||||
|
, m_dels(k.m_dels)
|
||||||
|
, m_keycontext(k.m_keycontext)
|
||||||
|
{
|
||||||
|
switch (m_type) {
|
||||||
|
case KeyType::Command:
|
||||||
|
m_command = k.m_command;
|
||||||
|
m_params = k.m_params;
|
||||||
|
break;
|
||||||
|
case KeyType::Tool:
|
||||||
|
case KeyType::Quicktool: m_tool = k.m_tool; break;
|
||||||
|
case KeyType::Action: m_action = k.m_action; break;
|
||||||
|
case KeyType::WheelAction:
|
||||||
|
m_action = k.m_action;
|
||||||
|
m_wheelAction = k.m_wheelAction;
|
||||||
|
break;
|
||||||
|
case KeyType::DragAction:
|
||||||
|
m_action = k.m_action;
|
||||||
|
m_wheelAction = k.m_wheelAction;
|
||||||
|
m_dragVector = k.m_dragVector;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Key(Command* command, const Params& params, const KeyContext keyContext)
|
||||||
|
: m_type(KeyType::Command)
|
||||||
|
, m_keycontext(keyContext)
|
||||||
|
, m_command(command)
|
||||||
|
, m_params(params)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Key(const KeyType type, tools::Tool* tool)
|
||||||
|
: m_type(type)
|
||||||
|
, m_keycontext(KeyContext::Any)
|
||||||
|
, m_tool(tool)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Key(const KeyAction action, const KeyContext keyContext)
|
||||||
|
: m_type(KeyType::Action)
|
||||||
|
, m_keycontext(keyContext)
|
||||||
|
, m_action(action)
|
||||||
|
{
|
||||||
|
if (m_keycontext != KeyContext::Any)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Automatic key context
|
||||||
|
switch (action) {
|
||||||
|
case KeyAction::None: m_keycontext = KeyContext::Any; break;
|
||||||
|
case KeyAction::CopySelection:
|
||||||
|
case KeyAction::SnapToGrid:
|
||||||
|
case KeyAction::LockAxis:
|
||||||
|
case KeyAction::FineControl: m_keycontext = KeyContext::TranslatingSelection; break;
|
||||||
|
case KeyAction::AngleSnap: m_keycontext = KeyContext::RotatingSelection; break;
|
||||||
|
case KeyAction::MaintainAspectRatio:
|
||||||
|
case KeyAction::ScaleFromCenter: m_keycontext = KeyContext::ScalingSelection; break;
|
||||||
|
case KeyAction::AddSelection:
|
||||||
|
case KeyAction::SubtractSelection:
|
||||||
|
case KeyAction::IntersectSelection: m_keycontext = KeyContext::SelectionTool; break;
|
||||||
|
case KeyAction::AutoSelectLayer: m_keycontext = KeyContext::MoveTool; break;
|
||||||
|
case KeyAction::StraightLineFromLastPoint:
|
||||||
|
case KeyAction::AngleSnapFromLastPoint: m_keycontext = KeyContext::FreehandTool; break;
|
||||||
|
case KeyAction::MoveOrigin:
|
||||||
|
case KeyAction::SquareAspect:
|
||||||
|
case KeyAction::DrawFromCenter:
|
||||||
|
case KeyAction::RotateShape: m_keycontext = KeyContext::ShapeTool; break;
|
||||||
|
case KeyAction::LeftMouseButton:
|
||||||
|
case KeyAction::RightMouseButton: m_keycontext = KeyContext::Any; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Key(const WheelAction wheelAction)
|
||||||
|
: m_type(KeyType::WheelAction)
|
||||||
|
, m_keycontext(KeyContext::MouseWheel)
|
||||||
|
, m_action(KeyAction::None)
|
||||||
|
, m_wheelAction(wheelAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
KeyPtr Key::MakeDragAction(WheelAction dragAction)
|
||||||
|
{
|
||||||
|
KeyPtr k(new Key(dragAction));
|
||||||
|
k->m_type = KeyType::DragAction;
|
||||||
|
k->m_keycontext = KeyContext::Any;
|
||||||
|
k->m_dragVector = wheel_actions()[(int)dragAction].vector;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ui::Shortcuts& Key::shortcuts() const
|
||||||
|
{
|
||||||
|
if (!m_shortcuts) {
|
||||||
|
m_shortcuts = std::make_unique<ui::Shortcuts>();
|
||||||
|
|
||||||
|
// Add default keys
|
||||||
|
for (const auto& kv : m_adds) {
|
||||||
|
if (kv.first == KeySource::Original)
|
||||||
|
m_shortcuts->add(kv.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete/add extension-defined keys
|
||||||
|
for (const auto& kv : m_dels) {
|
||||||
|
if (kv.first == KeySource::ExtensionDefined)
|
||||||
|
m_shortcuts->remove(kv.second);
|
||||||
|
else {
|
||||||
|
ASSERT(kv.first != KeySource::Original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& kv : m_adds) {
|
||||||
|
if (kv.first == KeySource::ExtensionDefined)
|
||||||
|
m_shortcuts->add(kv.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete/add user-defined keys
|
||||||
|
for (const auto& kv : m_dels) {
|
||||||
|
if (kv.first == KeySource::UserDefined)
|
||||||
|
m_shortcuts->remove(kv.second);
|
||||||
|
}
|
||||||
|
for (const auto& kv : m_adds) {
|
||||||
|
if (kv.first == KeySource::UserDefined)
|
||||||
|
m_shortcuts->add(kv.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return *m_shortcuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShortcuts& globalKeys)
|
||||||
|
{
|
||||||
|
m_adds.emplace_back(source, shortcut);
|
||||||
|
m_shortcuts.reset();
|
||||||
|
|
||||||
|
// Remove the shortcut from other commands
|
||||||
|
if (source == KeySource::ExtensionDefined || source == KeySource::UserDefined) {
|
||||||
|
erase_shortcut(m_dels, source, shortcut);
|
||||||
|
|
||||||
|
globalKeys.disableShortcut(shortcut, source, m_keycontext, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ui::Shortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
|
||||||
|
{
|
||||||
|
if (const auto* keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
|
||||||
|
for (const Shortcut& shortcut : shortcuts()) {
|
||||||
|
if (shortcut.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
|
||||||
|
(m_keycontext == KeyContext::Any || match_key_context(m_keycontext, keyContext))) {
|
||||||
|
return &shortcut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (const auto* mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
|
||||||
|
for (const Shortcut& shortcut : shortcuts()) {
|
||||||
|
if ((shortcut.modifiers() == mouseMsg->modifiers()) &&
|
||||||
|
(m_keycontext == KeyContext::Any ||
|
||||||
|
// TODO we could have multiple mouse wheel key-context,
|
||||||
|
// like "sprite editor" context, or "timeline" context,
|
||||||
|
// etc.
|
||||||
|
m_keycontext == KeyContext::MouseWheel)) {
|
||||||
|
return &shortcut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ui::Shortcut* Key::isPressed(const Message* msg) const
|
||||||
|
{
|
||||||
|
return isPressed(msg, KeyboardShortcuts::getCurrentKeyContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Key::isPressed() const
|
||||||
|
{
|
||||||
|
const auto& ss = this->shortcuts();
|
||||||
|
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
|
||||||
|
return shortcut.isPressed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Key::isLooselyPressed() const
|
||||||
|
{
|
||||||
|
const auto& ss = this->shortcuts();
|
||||||
|
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
|
||||||
|
return shortcut.isLooselyPressed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Key::isCommandListed() const
|
||||||
|
{
|
||||||
|
return type() == KeyType::Command && command()->isListed(params());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Key::hasShortcut(const ui::Shortcut& shortcut) const
|
||||||
|
{
|
||||||
|
return shortcuts().has(shortcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Key::hasUserDefinedShortcuts() const
|
||||||
|
{
|
||||||
|
return std::any_of(m_adds.begin(), m_adds.end(), [](const auto& kv) {
|
||||||
|
return (kv.first == KeySource::UserDefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key::disableShortcut(const ui::Shortcut& shortcut, const KeySource source)
|
||||||
|
{
|
||||||
|
// It doesn't make sense that the default keyboard shortcuts file
|
||||||
|
// (gui.xml) removes some shortcut.
|
||||||
|
ASSERT(source != KeySource::Original);
|
||||||
|
|
||||||
|
erase_shortcut(m_adds, source, shortcut);
|
||||||
|
erase_shortcut(m_dels, source, shortcut);
|
||||||
|
|
||||||
|
m_dels.emplace_back(source, shortcut);
|
||||||
|
m_shortcuts.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key::reset()
|
||||||
|
{
|
||||||
|
erase_shortcuts(m_adds, KeySource::UserDefined);
|
||||||
|
erase_shortcuts(m_dels, KeySource::UserDefined);
|
||||||
|
m_shortcuts.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key::copyOriginalToUser()
|
||||||
|
{
|
||||||
|
// Erase all user-defined keys
|
||||||
|
erase_shortcuts(m_adds, KeySource::UserDefined);
|
||||||
|
erase_shortcuts(m_dels, KeySource::UserDefined);
|
||||||
|
|
||||||
|
// Then copy all original & extension-defined keys as user-defined
|
||||||
|
auto copy = m_adds;
|
||||||
|
for (const auto& kv : copy)
|
||||||
|
m_adds.emplace_back(KeySource::UserDefined, kv.second);
|
||||||
|
m_shortcuts.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Key::triggerString() const
|
||||||
|
{
|
||||||
|
switch (m_type) {
|
||||||
|
case KeyType::Command: m_command->loadParams(m_params); return m_command->friendlyName();
|
||||||
|
case KeyType::Tool:
|
||||||
|
case KeyType::Quicktool: {
|
||||||
|
std::string text = m_tool->getText();
|
||||||
|
if (m_type == KeyType::Quicktool)
|
||||||
|
text += " (quick)";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
case KeyType::Action: return get_user_friendly_string_for_keyaction(m_action, m_keycontext);
|
||||||
|
case KeyType::WheelAction:
|
||||||
|
case KeyType::DragAction: return get_user_friendly_string_for_wheelaction(m_wheelAction);
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset_key_tables_that_depends_on_language()
|
||||||
|
{
|
||||||
|
g_actions.clear();
|
||||||
|
g_wheel_actions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string key_tooltip(const char* str, const app::Key* key)
|
||||||
|
{
|
||||||
|
std::string res;
|
||||||
|
if (str)
|
||||||
|
res += str;
|
||||||
|
if (key && !key->shortcuts().empty()) {
|
||||||
|
res += " (";
|
||||||
|
res += key->shortcuts().front().toString();
|
||||||
|
res += ")";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string convert_keycontext_to_user_friendly_string(const KeyContext keyctx)
|
||||||
|
{
|
||||||
|
switch (keyctx) {
|
||||||
|
case KeyContext::Any: return {};
|
||||||
|
case KeyContext::Normal: return I18N_KEY(key_context_normal);
|
||||||
|
case KeyContext::SelectionTool: return I18N_KEY(key_context_selection);
|
||||||
|
case KeyContext::TranslatingSelection: return I18N_KEY(key_context_translating_selection);
|
||||||
|
case KeyContext::ScalingSelection: return I18N_KEY(key_context_scaling_selection);
|
||||||
|
case KeyContext::RotatingSelection: return I18N_KEY(key_context_rotating_selection);
|
||||||
|
case KeyContext::MoveTool: return I18N_KEY(key_context_move_tool);
|
||||||
|
case KeyContext::FreehandTool: return I18N_KEY(key_context_freehand_tool);
|
||||||
|
case KeyContext::ShapeTool: return I18N_KEY(key_context_shape_tool);
|
||||||
|
case KeyContext::FramesSelection: return I18N_KEY(key_context_frames_selection);
|
||||||
|
case KeyContext::Transformation: return I18N_KEY(key_context_transformation);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
|
@ -178,7 +178,13 @@ private:
|
||||||
DragVector m_dragVector; // for KeyType::DragAction
|
DragVector m_dragVector; // for KeyType::DragAction
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string convertKeyContextToUserFriendlyString(KeyContext keyContext);
|
// Clears collection with strings that depends on the current
|
||||||
|
// language, so they can be reconstructed when they are needed with a
|
||||||
|
// new selected language.
|
||||||
|
void reset_key_tables_that_depends_on_language();
|
||||||
|
|
||||||
|
std::string key_tooltip(const char* str, const Key* key);
|
||||||
|
std::string convert_keycontext_to_user_friendly_string(KeyContext keyctx);
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
||||||
|
@ -194,6 +200,11 @@ app::WheelAction convert_to(const std::string& from);
|
||||||
template<>
|
template<>
|
||||||
std::string convert_to(const app::WheelAction& from);
|
std::string convert_to(const app::WheelAction& from);
|
||||||
|
|
||||||
|
template<>
|
||||||
|
app::KeyContext convert_to(const std::string& from);
|
||||||
|
template<>
|
||||||
|
std::string convert_to(const app::KeyContext& from);
|
||||||
|
|
||||||
} // namespace base
|
} // namespace base
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2023 Igara Studio S.A.
|
// Copyright (C) 2023-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2017 David Capello
|
// Copyright (C) 2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -23,8 +23,16 @@ enum class KeyContext {
|
||||||
ShapeTool,
|
ShapeTool,
|
||||||
MouseWheel,
|
MouseWheel,
|
||||||
FramesSelection,
|
FramesSelection,
|
||||||
|
Transformation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline bool match_key_context(const KeyContext a, const KeyContext b)
|
||||||
|
{
|
||||||
|
return (a == b) || (a == KeyContext::Any || b == KeyContext::Any) ||
|
||||||
|
((a == KeyContext::SelectionTool && b == KeyContext::Transformation) ||
|
||||||
|
(a == KeyContext::Transformation && b == KeyContext::SelectionTool));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include "app/tools/ink.h"
|
#include "app/tools/ink.h"
|
||||||
#include "app/tools/tool.h"
|
#include "app/tools/tool.h"
|
||||||
#include "app/tools/tool_box.h"
|
#include "app/tools/tool_box.h"
|
||||||
|
#include "app/ui/editor/editor.h"
|
||||||
#include "app/ui/key.h"
|
#include "app/ui/key.h"
|
||||||
#include "app/ui/timeline/timeline.h"
|
#include "app/ui/timeline/timeline.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
|
@ -39,171 +40,10 @@
|
||||||
|
|
||||||
#define XML_KEYBOARD_FILE_VERSION "1"
|
#define XML_KEYBOARD_FILE_VERSION "1"
|
||||||
|
|
||||||
#define I18N_KEY(a) app::Strings::keyboard_shortcuts_##a()
|
|
||||||
|
|
||||||
using namespace tinyxml2;
|
using namespace tinyxml2;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct KeyShortcutAction {
|
|
||||||
const char* name;
|
|
||||||
std::string userfriendly;
|
|
||||||
app::KeyAction action;
|
|
||||||
app::KeyContext context;
|
|
||||||
};
|
|
||||||
|
|
||||||
static std::vector<KeyShortcutAction> g_actions;
|
|
||||||
|
|
||||||
static const std::vector<KeyShortcutAction>& actions()
|
|
||||||
{
|
|
||||||
if (g_actions.empty()) {
|
|
||||||
g_actions = std::vector<KeyShortcutAction>{
|
|
||||||
{ "CopySelection",
|
|
||||||
I18N_KEY(copy_selection),
|
|
||||||
app::KeyAction::CopySelection,
|
|
||||||
app::KeyContext::TranslatingSelection },
|
|
||||||
{ "SnapToGrid",
|
|
||||||
I18N_KEY(snap_to_grid),
|
|
||||||
app::KeyAction::SnapToGrid,
|
|
||||||
app::KeyContext::TranslatingSelection },
|
|
||||||
{ "LockAxis",
|
|
||||||
I18N_KEY(lock_axis),
|
|
||||||
app::KeyAction::LockAxis,
|
|
||||||
app::KeyContext::TranslatingSelection },
|
|
||||||
{ "FineControl",
|
|
||||||
I18N_KEY(fine_translating),
|
|
||||||
app::KeyAction::FineControl,
|
|
||||||
app::KeyContext::TranslatingSelection },
|
|
||||||
{ "MaintainAspectRatio",
|
|
||||||
I18N_KEY(maintain_aspect_ratio),
|
|
||||||
app::KeyAction::MaintainAspectRatio,
|
|
||||||
app::KeyContext::ScalingSelection },
|
|
||||||
{ "ScaleFromCenter",
|
|
||||||
I18N_KEY(scale_from_center),
|
|
||||||
app::KeyAction::ScaleFromCenter,
|
|
||||||
app::KeyContext::ScalingSelection },
|
|
||||||
{ "FineControl",
|
|
||||||
I18N_KEY(fine_scaling),
|
|
||||||
app::KeyAction::FineControl,
|
|
||||||
app::KeyContext::ScalingSelection },
|
|
||||||
{ "AngleSnap",
|
|
||||||
I18N_KEY(angle_snap),
|
|
||||||
app::KeyAction::AngleSnap,
|
|
||||||
app::KeyContext::RotatingSelection },
|
|
||||||
{ "AddSelection",
|
|
||||||
I18N_KEY(add_selection),
|
|
||||||
app::KeyAction::AddSelection,
|
|
||||||
app::KeyContext::SelectionTool },
|
|
||||||
{ "SubtractSelection",
|
|
||||||
I18N_KEY(subtract_selection),
|
|
||||||
app::KeyAction::SubtractSelection,
|
|
||||||
app::KeyContext::SelectionTool },
|
|
||||||
{ "IntersectSelection",
|
|
||||||
I18N_KEY(intersect_selection),
|
|
||||||
app::KeyAction::IntersectSelection,
|
|
||||||
app::KeyContext::SelectionTool },
|
|
||||||
{ "AutoSelectLayer",
|
|
||||||
I18N_KEY(auto_select_layer),
|
|
||||||
app::KeyAction::AutoSelectLayer,
|
|
||||||
app::KeyContext::MoveTool },
|
|
||||||
{ "StraightLineFromLastPoint",
|
|
||||||
I18N_KEY(line_from_last_point),
|
|
||||||
app::KeyAction::StraightLineFromLastPoint,
|
|
||||||
app::KeyContext::FreehandTool },
|
|
||||||
{ "AngleSnapFromLastPoint",
|
|
||||||
I18N_KEY(angle_from_last_point),
|
|
||||||
app::KeyAction::AngleSnapFromLastPoint,
|
|
||||||
app::KeyContext::FreehandTool },
|
|
||||||
{ "MoveOrigin",
|
|
||||||
I18N_KEY(move_origin),
|
|
||||||
app::KeyAction::MoveOrigin,
|
|
||||||
app::KeyContext::ShapeTool },
|
|
||||||
{ "SquareAspect",
|
|
||||||
I18N_KEY(square_aspect),
|
|
||||||
app::KeyAction::SquareAspect,
|
|
||||||
app::KeyContext::ShapeTool },
|
|
||||||
{ "DrawFromCenter",
|
|
||||||
I18N_KEY(draw_from_center),
|
|
||||||
app::KeyAction::DrawFromCenter,
|
|
||||||
app::KeyContext::ShapeTool },
|
|
||||||
{ "RotateShape",
|
|
||||||
I18N_KEY(rotate_shape),
|
|
||||||
app::KeyAction::RotateShape,
|
|
||||||
app::KeyContext::ShapeTool },
|
|
||||||
{ "LeftMouseButton",
|
|
||||||
I18N_KEY(trigger_left_mouse_button),
|
|
||||||
app::KeyAction::LeftMouseButton,
|
|
||||||
app::KeyContext::Any },
|
|
||||||
{ "RightMouseButton",
|
|
||||||
I18N_KEY(trigger_right_mouse_button),
|
|
||||||
app::KeyAction::RightMouseButton,
|
|
||||||
app::KeyContext::Any }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return g_actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
static struct {
|
|
||||||
const char* name;
|
|
||||||
app::KeyContext context;
|
|
||||||
} g_contexts[] = {
|
|
||||||
{ "", app::KeyContext::Any },
|
|
||||||
{ "Normal", app::KeyContext::Normal },
|
|
||||||
{ "Selection", app::KeyContext::SelectionTool },
|
|
||||||
{ "TranslatingSelection", app::KeyContext::TranslatingSelection },
|
|
||||||
{ "ScalingSelection", app::KeyContext::ScalingSelection },
|
|
||||||
{ "RotatingSelection", app::KeyContext::RotatingSelection },
|
|
||||||
{ "MoveTool", app::KeyContext::MoveTool },
|
|
||||||
{ "FreehandTool", app::KeyContext::FreehandTool },
|
|
||||||
{ "ShapeTool", app::KeyContext::ShapeTool },
|
|
||||||
{ "FramesSelection", app::KeyContext::FramesSelection },
|
|
||||||
{ NULL, app::KeyContext::Any }
|
|
||||||
};
|
|
||||||
|
|
||||||
using Vec = app::DragVector;
|
|
||||||
|
|
||||||
struct KeyShortcutWheelAction {
|
|
||||||
const char* name;
|
|
||||||
const std::string userfriendly;
|
|
||||||
Vec vector;
|
|
||||||
};
|
|
||||||
|
|
||||||
static std::vector<KeyShortcutWheelAction> g_wheel_actions;
|
|
||||||
|
|
||||||
static const std::vector<KeyShortcutWheelAction>& wheel_actions()
|
|
||||||
{
|
|
||||||
if (g_wheel_actions.empty()) {
|
|
||||||
g_wheel_actions = std::vector<KeyShortcutWheelAction>{
|
|
||||||
{ "", "", Vec(0.0, 0.0) },
|
|
||||||
{ "Zoom", I18N_KEY(zoom), Vec(8.0, 0.0) },
|
|
||||||
{ "VScroll", I18N_KEY(scroll_vertically), Vec(4.0, 0.0) },
|
|
||||||
{ "HScroll", I18N_KEY(scroll_horizontally), Vec(4.0, 0.0) },
|
|
||||||
{ "FgColor", I18N_KEY(fg_color), Vec(8.0, 0.0) },
|
|
||||||
{ "BgColor", I18N_KEY(bg_color), Vec(8.0, 0.0) },
|
|
||||||
{ "FgTile", I18N_KEY(fg_tile), Vec(8.0, 0.0) },
|
|
||||||
{ "BgTile", I18N_KEY(bg_tile), Vec(8.0, 0.0) },
|
|
||||||
{ "Frame", I18N_KEY(change_frame), Vec(16.0, 0.0) },
|
|
||||||
{ "BrushSize", I18N_KEY(change_brush_size), Vec(4.0, 0.0) },
|
|
||||||
{ "BrushAngle", I18N_KEY(change_brush_angle), Vec(-4.0, 0.0) },
|
|
||||||
{ "ToolSameGroup", I18N_KEY(change_tool_same_group), Vec(8.0, 0.0) },
|
|
||||||
{ "ToolOtherGroup", I18N_KEY(change_tool), Vec(0.0, -8.0) },
|
|
||||||
{ "Layer", I18N_KEY(change_layer), Vec(0.0, 8.0) },
|
|
||||||
{ "InkType", I18N_KEY(change_ink_type), Vec(0.0, -16.0) },
|
|
||||||
{ "InkOpacity", I18N_KEY(change_ink_opacity), Vec(0.0, 1.0) },
|
|
||||||
{ "LayerOpacity", I18N_KEY(change_layer_opacity), Vec(0.0, 1.0) },
|
|
||||||
{ "CelOpacity", I18N_KEY(change_cel_opacity), Vec(0.0, 1.0) },
|
|
||||||
{ "Alpha", I18N_KEY(color_alpha), Vec(4.0, 0.0) },
|
|
||||||
{ "HslHue", I18N_KEY(color_hsl_hue), Vec(1.0, 0.0) },
|
|
||||||
{ "HslSaturation", I18N_KEY(color_hsl_saturation), Vec(4.0, 0.0) },
|
|
||||||
{ "HslLightness", I18N_KEY(color_hsl_lightness), Vec(0.0, 4.0) },
|
|
||||||
{ "HsvHue", I18N_KEY(color_hsv_hue), Vec(1.0, 0.0) },
|
|
||||||
{ "HsvSaturation", I18N_KEY(color_hsv_saturation), Vec(4.0, 0.0) },
|
|
||||||
{ "HsvValue", I18N_KEY(color_hsv_value), Vec(0.0, 4.0) }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return g_wheel_actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* get_shortcut(XMLElement* elem)
|
const char* get_shortcut(XMLElement* elem)
|
||||||
{
|
{
|
||||||
const char* shortcut = NULL;
|
const char* shortcut = NULL;
|
||||||
|
@ -225,384 +65,12 @@ const char* get_shortcut(XMLElement* elem)
|
||||||
return shortcut;
|
return shortcut;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string get_user_friendly_string_for_keyaction(app::KeyAction action, app::KeyContext context)
|
} // namespace
|
||||||
{
|
|
||||||
for (const auto& a : actions()) {
|
|
||||||
if (action == a.action && context == a.context)
|
|
||||||
return a.userfriendly;
|
|
||||||
}
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelAction)
|
|
||||||
{
|
|
||||||
int c = int(wheelAction);
|
|
||||||
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last)) {
|
|
||||||
return wheel_actions()[c].userfriendly;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
void erase_shortcut(app::KeySourceShortcutList& kvs,
|
|
||||||
const app::KeySource source,
|
|
||||||
const ui::Shortcut& shortcut)
|
|
||||||
{
|
|
||||||
for (auto it = kvs.begin(); it != kvs.end();) {
|
|
||||||
auto& kv = *it;
|
|
||||||
if (kv.first == source && kv.second == shortcut) {
|
|
||||||
it = kvs.erase(it);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void erase_shortcuts(app::KeySourceShortcutList& kvs, const app::KeySource source)
|
|
||||||
{
|
|
||||||
for (auto it = kvs.begin(); it != kvs.end();) {
|
|
||||||
auto& kv = *it;
|
|
||||||
if (kv.first == source) {
|
|
||||||
it = kvs.erase(it);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // anonymous namespace
|
|
||||||
|
|
||||||
namespace base {
|
|
||||||
|
|
||||||
template<>
|
|
||||||
app::KeyAction convert_to(const std::string& from)
|
|
||||||
{
|
|
||||||
for (const auto& a : actions()) {
|
|
||||||
if (from == a.name)
|
|
||||||
return a.action;
|
|
||||||
}
|
|
||||||
return app::KeyAction::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<>
|
|
||||||
std::string convert_to(const app::KeyAction& from)
|
|
||||||
{
|
|
||||||
for (const auto& a : actions()) {
|
|
||||||
if (from == a.action)
|
|
||||||
return a.name;
|
|
||||||
}
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
template<>
|
|
||||||
app::WheelAction convert_to(const std::string& from)
|
|
||||||
{
|
|
||||||
for (int c = int(app::WheelAction::First); c <= int(app::WheelAction::Last); ++c) {
|
|
||||||
if (from == wheel_actions()[c].name)
|
|
||||||
return (app::WheelAction)c;
|
|
||||||
}
|
|
||||||
return app::WheelAction::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<>
|
|
||||||
std::string convert_to(const app::WheelAction& from)
|
|
||||||
{
|
|
||||||
int c = int(from);
|
|
||||||
if (c >= int(app::WheelAction::First) && c <= int(app::WheelAction::Last)) {
|
|
||||||
return wheel_actions()[c].name;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
template<>
|
|
||||||
app::KeyContext convert_to(const std::string& from)
|
|
||||||
{
|
|
||||||
for (int c = 0; g_contexts[c].name; ++c) {
|
|
||||||
if (from == g_contexts[c].name)
|
|
||||||
return g_contexts[c].context;
|
|
||||||
}
|
|
||||||
return app::KeyContext::Any;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<>
|
|
||||||
std::string convert_to(const app::KeyContext& from)
|
|
||||||
{
|
|
||||||
for (int c = 0; g_contexts[c].name; ++c) {
|
|
||||||
if (from == g_contexts[c].context)
|
|
||||||
return g_contexts[c].name;
|
|
||||||
}
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace base
|
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
using namespace ui;
|
using namespace ui;
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
|
||||||
// Key
|
|
||||||
|
|
||||||
Key::Key(const Key& k)
|
|
||||||
: m_type(k.m_type)
|
|
||||||
, m_adds(k.m_adds)
|
|
||||||
, m_dels(k.m_dels)
|
|
||||||
, m_keycontext(k.m_keycontext)
|
|
||||||
{
|
|
||||||
switch (m_type) {
|
|
||||||
case KeyType::Command:
|
|
||||||
m_command = k.m_command;
|
|
||||||
m_params = k.m_params;
|
|
||||||
break;
|
|
||||||
case KeyType::Tool:
|
|
||||||
case KeyType::Quicktool: m_tool = k.m_tool; break;
|
|
||||||
case KeyType::Action: m_action = k.m_action; break;
|
|
||||||
case KeyType::WheelAction:
|
|
||||||
m_action = k.m_action;
|
|
||||||
m_wheelAction = k.m_wheelAction;
|
|
||||||
break;
|
|
||||||
case KeyType::DragAction:
|
|
||||||
m_action = k.m_action;
|
|
||||||
m_wheelAction = k.m_wheelAction;
|
|
||||||
m_dragVector = k.m_dragVector;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::Key(Command* command, const Params& params, const KeyContext keyContext)
|
|
||||||
: m_type(KeyType::Command)
|
|
||||||
, m_keycontext(keyContext)
|
|
||||||
, m_command(command)
|
|
||||||
, m_params(params)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::Key(const KeyType type, tools::Tool* tool)
|
|
||||||
: m_type(type)
|
|
||||||
, m_keycontext(KeyContext::Any)
|
|
||||||
, m_tool(tool)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::Key(const KeyAction action, const KeyContext keyContext)
|
|
||||||
: m_type(KeyType::Action)
|
|
||||||
, m_keycontext(keyContext)
|
|
||||||
, m_action(action)
|
|
||||||
{
|
|
||||||
if (m_keycontext != KeyContext::Any)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Automatic key context
|
|
||||||
switch (action) {
|
|
||||||
case KeyAction::None: m_keycontext = KeyContext::Any; break;
|
|
||||||
case KeyAction::CopySelection:
|
|
||||||
case KeyAction::SnapToGrid:
|
|
||||||
case KeyAction::LockAxis:
|
|
||||||
case KeyAction::FineControl: m_keycontext = KeyContext::TranslatingSelection; break;
|
|
||||||
case KeyAction::AngleSnap: m_keycontext = KeyContext::RotatingSelection; break;
|
|
||||||
case KeyAction::MaintainAspectRatio:
|
|
||||||
case KeyAction::ScaleFromCenter: m_keycontext = KeyContext::ScalingSelection; break;
|
|
||||||
case KeyAction::AddSelection:
|
|
||||||
case KeyAction::SubtractSelection:
|
|
||||||
case KeyAction::IntersectSelection: m_keycontext = KeyContext::SelectionTool; break;
|
|
||||||
case KeyAction::AutoSelectLayer: m_keycontext = KeyContext::MoveTool; break;
|
|
||||||
case KeyAction::StraightLineFromLastPoint:
|
|
||||||
case KeyAction::AngleSnapFromLastPoint: m_keycontext = KeyContext::FreehandTool; break;
|
|
||||||
case KeyAction::MoveOrigin:
|
|
||||||
case KeyAction::SquareAspect:
|
|
||||||
case KeyAction::DrawFromCenter:
|
|
||||||
case KeyAction::RotateShape: m_keycontext = KeyContext::ShapeTool; break;
|
|
||||||
case KeyAction::LeftMouseButton: m_keycontext = KeyContext::Any; break;
|
|
||||||
case KeyAction::RightMouseButton: m_keycontext = KeyContext::Any; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::Key(const WheelAction wheelAction)
|
|
||||||
: m_type(KeyType::WheelAction)
|
|
||||||
, m_keycontext(KeyContext::MouseWheel)
|
|
||||||
, m_action(KeyAction::None)
|
|
||||||
, m_wheelAction(wheelAction)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// static
|
|
||||||
KeyPtr Key::MakeDragAction(WheelAction dragAction)
|
|
||||||
{
|
|
||||||
KeyPtr k(new Key(dragAction));
|
|
||||||
k->m_type = KeyType::DragAction;
|
|
||||||
k->m_keycontext = KeyContext::Any;
|
|
||||||
k->m_dragVector = wheel_actions()[(int)dragAction].vector;
|
|
||||||
return k;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ui::Shortcuts& Key::shortcuts() const
|
|
||||||
{
|
|
||||||
if (!m_shortcuts) {
|
|
||||||
m_shortcuts = std::make_unique<ui::Shortcuts>();
|
|
||||||
|
|
||||||
// Add default keys
|
|
||||||
for (const auto& kv : m_adds) {
|
|
||||||
if (kv.first == KeySource::Original)
|
|
||||||
m_shortcuts->add(kv.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete/add extension-defined keys
|
|
||||||
for (const auto& kv : m_dels) {
|
|
||||||
if (kv.first == KeySource::ExtensionDefined)
|
|
||||||
m_shortcuts->remove(kv.second);
|
|
||||||
else {
|
|
||||||
ASSERT(kv.first != KeySource::Original);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto& kv : m_adds) {
|
|
||||||
if (kv.first == KeySource::ExtensionDefined)
|
|
||||||
m_shortcuts->add(kv.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete/add user-defined keys
|
|
||||||
for (const auto& kv : m_dels) {
|
|
||||||
if (kv.first == KeySource::UserDefined)
|
|
||||||
m_shortcuts->remove(kv.second);
|
|
||||||
}
|
|
||||||
for (const auto& kv : m_adds) {
|
|
||||||
if (kv.first == KeySource::UserDefined)
|
|
||||||
m_shortcuts->add(kv.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return *m_shortcuts;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShortcuts& globalKeys)
|
|
||||||
{
|
|
||||||
m_adds.emplace_back(source, shortcut);
|
|
||||||
m_shortcuts.reset();
|
|
||||||
|
|
||||||
// Remove the shortcut from other commands
|
|
||||||
if (source == KeySource::ExtensionDefined || source == KeySource::UserDefined) {
|
|
||||||
erase_shortcut(m_dels, source, shortcut);
|
|
||||||
|
|
||||||
globalKeys.disableShortcut(shortcut, source, m_keycontext, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ui::Shortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
|
|
||||||
{
|
|
||||||
if (const auto* keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
|
|
||||||
for (const Shortcut& shortcut : shortcuts()) {
|
|
||||||
if (shortcut.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
|
|
||||||
(m_keycontext == KeyContext::Any || m_keycontext == keyContext)) {
|
|
||||||
return &shortcut;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (const auto* mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
|
|
||||||
for (const Shortcut& shortcut : shortcuts()) {
|
|
||||||
if ((shortcut.modifiers() == mouseMsg->modifiers()) &&
|
|
||||||
(m_keycontext == KeyContext::Any ||
|
|
||||||
// TODO we could have multiple mouse wheel key-context,
|
|
||||||
// like "sprite editor" context, or "timeline" context,
|
|
||||||
// etc.
|
|
||||||
m_keycontext == KeyContext::MouseWheel)) {
|
|
||||||
return &shortcut;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ui::Shortcut* Key::isPressed(const Message* msg) const
|
|
||||||
{
|
|
||||||
return isPressed(msg, KeyboardShortcuts::getCurrentKeyContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Key::isPressed() const
|
|
||||||
{
|
|
||||||
const auto& ss = this->shortcuts();
|
|
||||||
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
|
|
||||||
return shortcut.isPressed();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Key::isLooselyPressed() const
|
|
||||||
{
|
|
||||||
const auto& ss = this->shortcuts();
|
|
||||||
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
|
|
||||||
return shortcut.isLooselyPressed();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Key::isCommandListed() const
|
|
||||||
{
|
|
||||||
return type() == KeyType::Command && command()->isListed(params());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Key::hasShortcut(const ui::Shortcut& shortcut) const
|
|
||||||
{
|
|
||||||
return shortcuts().has(shortcut);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Key::hasUserDefinedShortcuts() const
|
|
||||||
{
|
|
||||||
return std::any_of(m_adds.begin(), m_adds.end(), [](const auto& kv) {
|
|
||||||
return (kv.first == KeySource::UserDefined);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void Key::disableShortcut(const ui::Shortcut& shortcut, const KeySource source)
|
|
||||||
{
|
|
||||||
// It doesn't make sense that the default keyboard shortcuts file
|
|
||||||
// (gui.xml) removes some shortcut.
|
|
||||||
ASSERT(source != KeySource::Original);
|
|
||||||
|
|
||||||
erase_shortcut(m_adds, source, shortcut);
|
|
||||||
erase_shortcut(m_dels, source, shortcut);
|
|
||||||
|
|
||||||
m_dels.emplace_back(source, shortcut);
|
|
||||||
m_shortcuts.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Key::reset()
|
|
||||||
{
|
|
||||||
erase_shortcuts(m_adds, KeySource::UserDefined);
|
|
||||||
erase_shortcuts(m_dels, KeySource::UserDefined);
|
|
||||||
m_shortcuts.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Key::copyOriginalToUser()
|
|
||||||
{
|
|
||||||
// Erase all user-defined keys
|
|
||||||
erase_shortcuts(m_adds, KeySource::UserDefined);
|
|
||||||
erase_shortcuts(m_dels, KeySource::UserDefined);
|
|
||||||
|
|
||||||
// Then copy all original & extension-defined keys as user-defined
|
|
||||||
auto copy = m_adds;
|
|
||||||
for (const auto& kv : copy)
|
|
||||||
m_adds.emplace_back(KeySource::UserDefined, kv.second);
|
|
||||||
m_shortcuts.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Key::triggerString() const
|
|
||||||
{
|
|
||||||
switch (m_type) {
|
|
||||||
case KeyType::Command: m_command->loadParams(m_params); return m_command->friendlyName();
|
|
||||||
case KeyType::Tool:
|
|
||||||
case KeyType::Quicktool: {
|
|
||||||
std::string text = m_tool->getText();
|
|
||||||
if (m_type == KeyType::Quicktool)
|
|
||||||
text += " (quick)";
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
case KeyType::Action: return get_user_friendly_string_for_keyaction(m_action, m_keycontext);
|
|
||||||
case KeyType::WheelAction:
|
|
||||||
case KeyType::DragAction: return get_user_friendly_string_for_wheelaction(m_wheelAction);
|
|
||||||
}
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
|
||||||
// KeyboardShortcuts
|
|
||||||
|
|
||||||
static std::unique_ptr<KeyboardShortcuts> g_singleton;
|
static std::unique_ptr<KeyboardShortcuts> g_singleton;
|
||||||
|
|
||||||
// static
|
// static
|
||||||
|
@ -622,11 +90,7 @@ void KeyboardShortcuts::destroyInstance()
|
||||||
KeyboardShortcuts::KeyboardShortcuts()
|
KeyboardShortcuts::KeyboardShortcuts()
|
||||||
{
|
{
|
||||||
ASSERT(Strings::instance());
|
ASSERT(Strings::instance());
|
||||||
Strings::instance()->LanguageChange.connect([] {
|
Strings::instance()->LanguageChange.connect([] { reset_key_tables_that_depends_on_language(); });
|
||||||
// Clear collections so they are re-constructed with the new language
|
|
||||||
g_actions.clear();
|
|
||||||
g_wheel_actions.clear();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardShortcuts::~KeyboardShortcuts()
|
KeyboardShortcuts::~KeyboardShortcuts()
|
||||||
|
@ -1078,6 +542,12 @@ void KeyboardShortcuts::disableShortcut(const ui::Shortcut& shortcut,
|
||||||
// static
|
// static
|
||||||
KeyContext KeyboardShortcuts::getCurrentKeyContext()
|
KeyContext KeyboardShortcuts::getCurrentKeyContext()
|
||||||
{
|
{
|
||||||
|
// For shortcuts to Apply/Cancel transformation/moving pixels state.
|
||||||
|
auto* editor = Editor::activeEditor();
|
||||||
|
if (editor && editor->isMovingPixels()) {
|
||||||
|
return KeyContext::Transformation;
|
||||||
|
}
|
||||||
|
|
||||||
auto* ctx = UIContext::instance();
|
auto* ctx = UIContext::instance();
|
||||||
Doc* doc = ctx->activeDocument();
|
Doc* doc = ctx->activeDocument();
|
||||||
if (doc && doc->isMaskVisible() &&
|
if (doc && doc->isMaskVisible() &&
|
||||||
|
@ -1309,34 +779,4 @@ void KeyboardShortcuts::addMissingKeysForCommands()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string key_tooltip(const char* str, const app::Key* key)
|
|
||||||
{
|
|
||||||
std::string res;
|
|
||||||
if (str)
|
|
||||||
res += str;
|
|
||||||
if (key && !key->shortcuts().empty()) {
|
|
||||||
res += " (";
|
|
||||||
res += key->shortcuts().front().toString();
|
|
||||||
res += ")";
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string convertKeyContextToUserFriendlyString(KeyContext keyContext)
|
|
||||||
{
|
|
||||||
switch (keyContext) {
|
|
||||||
case KeyContext::Any: return std::string();
|
|
||||||
case KeyContext::Normal: return I18N_KEY(key_context_normal);
|
|
||||||
case KeyContext::SelectionTool: return I18N_KEY(key_context_selection);
|
|
||||||
case KeyContext::TranslatingSelection: return I18N_KEY(key_context_translating_selection);
|
|
||||||
case KeyContext::ScalingSelection: return I18N_KEY(key_context_scaling_selection);
|
|
||||||
case KeyContext::RotatingSelection: return I18N_KEY(key_context_rotating_selection);
|
|
||||||
case KeyContext::MoveTool: return I18N_KEY(key_context_move_tool);
|
|
||||||
case KeyContext::FreehandTool: return I18N_KEY(key_context_freehand_tool);
|
|
||||||
case KeyContext::ShapeTool: return I18N_KEY(key_context_shape_tool);
|
|
||||||
case KeyContext::FramesSelection: return I18N_KEY(key_context_frames_selection);
|
|
||||||
}
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
@ -85,8 +85,6 @@ private:
|
||||||
mutable Keys m_keys;
|
mutable Keys m_keys;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string key_tooltip(const char* str, const Key* key);
|
|
||||||
|
|
||||||
inline std::string key_tooltip(const char* str,
|
inline std::string key_tooltip(const char* str,
|
||||||
const char* commandName,
|
const char* commandName,
|
||||||
const Params& params = Params(),
|
const Params& params = Params(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2017 David Capello
|
// Copyright (C) 2017 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -181,8 +181,7 @@ void SliceWindow::onPivotChange()
|
||||||
void SliceWindow::onToggleUserData()
|
void SliceWindow::onToggleUserData()
|
||||||
{
|
{
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
remapWindow();
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
manager()->invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SliceWindow::onModifyField(ui::Entry* entry, const Mods mods)
|
void SliceWindow::onModifyField(ui::Entry* entry, const Mods mods)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2016 David Capello
|
// Copyright (C) 2001-2016 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -132,8 +132,7 @@ void TagWindow::onRepeatChange()
|
||||||
void TagWindow::onToggleUserData()
|
void TagWindow::onToggleUserData()
|
||||||
{
|
{
|
||||||
m_userDataView.toggleVisibility();
|
m_userDataView.toggleVisibility();
|
||||||
remapWindow();
|
expandWindow(gfx::Size(bounds().w, sizeHint().h));
|
||||||
manager()->invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
@ -4321,6 +4321,12 @@ void Timeline::clearAndInvalidateRange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Timeline::refresh()
|
||||||
|
{
|
||||||
|
regenerateRows();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
app::gen::GlobalPref::Timeline& Timeline::timelinePref() const
|
app::gen::GlobalPref::Timeline& Timeline::timelinePref() const
|
||||||
{
|
{
|
||||||
return Preferences::instance().timeline;
|
return Preferences::instance().timeline;
|
||||||
|
@ -4545,7 +4551,7 @@ void Timeline::onDrag(ui::DragEvent& e)
|
||||||
m_range.clearRange();
|
m_range.clearRange();
|
||||||
setHot(hitTest(nullptr, e.position()));
|
setHot(hitTest(nullptr, e.position()));
|
||||||
switch (m_hot.part) {
|
switch (m_hot.part) {
|
||||||
case PART_NOTHING: invalidate();
|
case PART_NOTHING: invalidate(); [[fallthrough]];
|
||||||
case PART_ROW:
|
case PART_ROW:
|
||||||
case PART_ROW_EYE_ICON:
|
case PART_ROW_EYE_ICON:
|
||||||
case PART_ROW_CONTINUOUS_ICON:
|
case PART_ROW_CONTINUOUS_ICON:
|
||||||
|
@ -4576,7 +4582,7 @@ void Timeline::onDrop(ui::DragEvent& e)
|
||||||
|
|
||||||
// Determine at which frame and layer the content was dropped on.
|
// Determine at which frame and layer the content was dropped on.
|
||||||
frame_t frame = m_frame;
|
frame_t frame = m_frame;
|
||||||
layer_t layerIndex = getLayerIndex(m_layer);
|
layer_t layerIndex = m_sprite->root()->getLayerIndex(m_layer);
|
||||||
InsertionPoint insert = InsertionPoint::BeforeLayer;
|
InsertionPoint insert = InsertionPoint::BeforeLayer;
|
||||||
DroppedOn droppedOn = DroppedOn::Unspecified;
|
DroppedOn droppedOn = DroppedOn::Unspecified;
|
||||||
TRACE("m_dropRange.type() %d\n", m_dropRange.type());
|
TRACE("m_dropRange.type() %d\n", m_dropRange.type());
|
||||||
|
@ -4602,7 +4608,7 @@ void Timeline::onDrop(ui::DragEvent& e)
|
||||||
break;
|
break;
|
||||||
case Range::kLayers:
|
case Range::kLayers:
|
||||||
droppedOn = DroppedOn::Layer;
|
droppedOn = DroppedOn::Layer;
|
||||||
if (m_dropTarget.vhit != DropTarget::VeryBottom) {
|
if (m_dropTarget.vhit != DropTarget::VeryBottom && !m_dropRange.selectedLayers().empty()) {
|
||||||
auto* selectedLayer = *m_dropRange.selectedLayers().begin();
|
auto* selectedLayer = *m_dropRange.selectedLayers().begin();
|
||||||
layerIndex = getLayerIndex(selectedLayer);
|
layerIndex = getLayerIndex(selectedLayer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,8 @@ public:
|
||||||
|
|
||||||
void clearAndInvalidateRange();
|
void clearAndInvalidateRange();
|
||||||
|
|
||||||
|
void refresh();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool onProcessMessage(ui::Message* msg) override;
|
bool onProcessMessage(ui::Message* msg) override;
|
||||||
void onInitTheme(ui::InitThemeEvent& ev) override;
|
void onInitTheme(ui::InitThemeEvent& ev) override;
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "app/app.h"
|
#include "app/app.h"
|
||||||
|
#include "app/cmd/add_slice.h"
|
||||||
#include "app/cmd/clear_mask.h"
|
#include "app/cmd/clear_mask.h"
|
||||||
#include "app/cmd/deselect_mask.h"
|
#include "app/cmd/deselect_mask.h"
|
||||||
#include "app/cmd/set_mask.h"
|
#include "app/cmd/set_mask.h"
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
#include "app/doc_api.h"
|
#include "app/doc_api.h"
|
||||||
#include "app/doc_range.h"
|
#include "app/doc_range.h"
|
||||||
#include "app/doc_range_ops.h"
|
#include "app/doc_range_ops.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/modules/gfx.h"
|
#include "app/modules/gfx.h"
|
||||||
#include "app/modules/gui.h"
|
#include "app/modules/gui.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
#include "app/util/cel_ops.h"
|
#include "app/util/cel_ops.h"
|
||||||
#include "app/util/clipboard.h"
|
#include "app/util/clipboard.h"
|
||||||
#include "app/util/new_image_from_mask.h"
|
#include "app/util/new_image_from_mask.h"
|
||||||
|
#include "app/util/slice_utils.h"
|
||||||
#include "clip/clip.h"
|
#include "clip/clip.h"
|
||||||
#include "doc/algorithm/shrink_bounds.h"
|
#include "doc/algorithm/shrink_bounds.h"
|
||||||
#include "doc/blend_image.h"
|
#include "doc/blend_image.h"
|
||||||
|
@ -114,6 +117,9 @@ struct Clipboard::Data {
|
||||||
// Selected set of layers/layers/cels
|
// Selected set of layers/layers/cels
|
||||||
ClipboardRange range;
|
ClipboardRange range;
|
||||||
|
|
||||||
|
// Selected slices
|
||||||
|
std::vector<Slice> slices;
|
||||||
|
|
||||||
Data() { range.observeUIContext(); }
|
Data() { range.observeUIContext(); }
|
||||||
|
|
||||||
~Data()
|
~Data()
|
||||||
|
@ -132,6 +138,7 @@ struct Clipboard::Data {
|
||||||
picks.clear();
|
picks.clear();
|
||||||
mask.reset();
|
mask.reset();
|
||||||
range.invalidate();
|
range.invalidate();
|
||||||
|
slices.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipboardFormat format() const
|
ClipboardFormat format() const
|
||||||
|
@ -146,6 +153,8 @@ struct Clipboard::Data {
|
||||||
return ClipboardFormat::PaletteEntries;
|
return ClipboardFormat::PaletteEntries;
|
||||||
else if (tileset && picks.picks())
|
else if (tileset && picks.picks())
|
||||||
return ClipboardFormat::Tileset;
|
return ClipboardFormat::Tileset;
|
||||||
|
else if (!slices.empty())
|
||||||
|
return ClipboardFormat::Slices;
|
||||||
else
|
else
|
||||||
return ClipboardFormat::None;
|
return ClipboardFormat::None;
|
||||||
}
|
}
|
||||||
|
@ -212,6 +221,7 @@ void Clipboard::setData(Image* image,
|
||||||
Mask* mask,
|
Mask* mask,
|
||||||
Palette* palette,
|
Palette* palette,
|
||||||
Tileset* tileset,
|
Tileset* tileset,
|
||||||
|
const std::vector<Slice*>* slices,
|
||||||
bool set_native_clipboard,
|
bool set_native_clipboard,
|
||||||
bool image_source_is_transparent)
|
bool image_source_is_transparent)
|
||||||
{
|
{
|
||||||
|
@ -226,6 +236,11 @@ void Clipboard::setData(Image* image,
|
||||||
else
|
else
|
||||||
m_data->image.reset(image);
|
m_data->image.reset(image);
|
||||||
|
|
||||||
|
if (slices) {
|
||||||
|
for (auto* slice : *slices)
|
||||||
|
m_data->slices.push_back(*slice);
|
||||||
|
}
|
||||||
|
|
||||||
if (set_native_clipboard && use_native_clipboard()) {
|
if (set_native_clipboard && use_native_clipboard()) {
|
||||||
// Copy tilemap to the native clipboard
|
// Copy tilemap to the native clipboard
|
||||||
if (isTilemap) {
|
if (isTilemap) {
|
||||||
|
@ -262,6 +277,7 @@ bool Clipboard::copyFromDocument(const Site& site, bool merged)
|
||||||
(mask ? new Mask(*mask) : nullptr),
|
(mask ? new Mask(*mask) : nullptr),
|
||||||
(pal ? new Palette(*pal) : nullptr),
|
(pal ? new Palette(*pal) : nullptr),
|
||||||
Tileset::MakeCopyCopyingImages(ts),
|
Tileset::MakeCopyCopyingImages(ts),
|
||||||
|
nullptr,
|
||||||
true, // set native clipboard
|
true, // set native clipboard
|
||||||
site.layer() && !site.layer()->isBackground());
|
site.layer() && !site.layer()->isBackground());
|
||||||
|
|
||||||
|
@ -277,6 +293,7 @@ bool Clipboard::copyFromDocument(const Site& site, bool merged)
|
||||||
(mask ? new Mask(*mask) : nullptr),
|
(mask ? new Mask(*mask) : nullptr),
|
||||||
(pal ? new Palette(*pal) : nullptr),
|
(pal ? new Palette(*pal) : nullptr),
|
||||||
nullptr,
|
nullptr,
|
||||||
|
nullptr,
|
||||||
true, // set native clipboard
|
true, // set native clipboard
|
||||||
site.layer() && !site.layer()->isBackground());
|
site.layer() && !site.layer()->isBackground());
|
||||||
|
|
||||||
|
@ -401,6 +418,7 @@ void Clipboard::copyImage(const Image* image, const Mask* mask, const Palette* p
|
||||||
(mask ? new Mask(*mask) : nullptr),
|
(mask ? new Mask(*mask) : nullptr),
|
||||||
(pal ? new Palette(*pal) : nullptr),
|
(pal ? new Palette(*pal) : nullptr),
|
||||||
nullptr,
|
nullptr,
|
||||||
|
nullptr,
|
||||||
App::instance()->isGui(),
|
App::instance()->isGui(),
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
@ -415,6 +433,7 @@ void Clipboard::copyTilemap(const Image* image,
|
||||||
(mask ? new Mask(*mask) : nullptr),
|
(mask ? new Mask(*mask) : nullptr),
|
||||||
(pal ? new Palette(*pal) : nullptr),
|
(pal ? new Palette(*pal) : nullptr),
|
||||||
Tileset::MakeCopyCopyingImages(tileset),
|
Tileset::MakeCopyCopyingImages(tileset),
|
||||||
|
nullptr,
|
||||||
true,
|
true,
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
@ -428,6 +447,7 @@ void Clipboard::copyPalette(const Palette* palette, const PalettePicks& picks)
|
||||||
nullptr,
|
nullptr,
|
||||||
new Palette(*palette),
|
new Palette(*palette),
|
||||||
nullptr,
|
nullptr,
|
||||||
|
nullptr,
|
||||||
false, // Don't touch the native clipboard now
|
false, // Don't touch the native clipboard now
|
||||||
false);
|
false);
|
||||||
|
|
||||||
|
@ -438,6 +458,20 @@ void Clipboard::copyPalette(const Palette* palette, const PalettePicks& picks)
|
||||||
m_data->picks = picks;
|
m_data->picks = picks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Clipboard::copySlices(const std::vector<Slice*> slices)
|
||||||
|
{
|
||||||
|
if (slices.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
setData(nullptr,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
&slices,
|
||||||
|
false, // Don't touch the native clipboard now
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* position)
|
void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* position)
|
||||||
{
|
{
|
||||||
const Site site = ctx->activeSite();
|
const Site site = ctx->activeSite();
|
||||||
|
@ -782,6 +816,26 @@ void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* po
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ClipboardFormat::Slices: {
|
||||||
|
auto& slices = m_data->slices;
|
||||||
|
|
||||||
|
if (slices.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ContextWriter writer(ctx);
|
||||||
|
Tx tx(writer, "Paste Slices");
|
||||||
|
editor->clearSlicesSelection();
|
||||||
|
for (auto& s : slices) {
|
||||||
|
Slice* slice = new Slice(s);
|
||||||
|
slice->setName(Strings::general_copy_of(slice->name()));
|
||||||
|
tx(new cmd::AddSlice(dstSpr, slice));
|
||||||
|
editor->selectSlice(slice);
|
||||||
|
}
|
||||||
|
tx.commit();
|
||||||
|
updateDstDoc = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update all editors/views showing this document
|
// Update all editors/views showing this document
|
||||||
|
@ -799,7 +853,7 @@ ImageRef Clipboard::getImage(Palette* palette)
|
||||||
Tileset* native_tileset = nullptr;
|
Tileset* native_tileset = nullptr;
|
||||||
getNativeBitmap(&native_image, &native_mask, &native_palette, &native_tileset);
|
getNativeBitmap(&native_image, &native_mask, &native_palette, &native_tileset);
|
||||||
if (native_image) {
|
if (native_image) {
|
||||||
setData(native_image, native_mask, native_palette, native_tileset, false, false);
|
setData(native_image, native_mask, native_palette, native_tileset, nullptr, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_data->palette && palette)
|
if (m_data->palette && palette)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
|
@ -23,6 +23,7 @@ class Image;
|
||||||
class Mask;
|
class Mask;
|
||||||
class Palette;
|
class Palette;
|
||||||
class PalettePicks;
|
class PalettePicks;
|
||||||
|
class Slice;
|
||||||
class Tileset;
|
class Tileset;
|
||||||
} // namespace doc
|
} // namespace doc
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ enum class ClipboardFormat {
|
||||||
PaletteEntries,
|
PaletteEntries,
|
||||||
Tilemap,
|
Tilemap,
|
||||||
Tileset,
|
Tileset,
|
||||||
|
Slices,
|
||||||
};
|
};
|
||||||
|
|
||||||
class Clipboard : public ui::ClipboardDelegate {
|
class Clipboard : public ui::ClipboardDelegate {
|
||||||
|
@ -74,6 +76,7 @@ public:
|
||||||
const doc::Palette* pal,
|
const doc::Palette* pal,
|
||||||
const doc::Tileset* tileset);
|
const doc::Tileset* tileset);
|
||||||
void copyPalette(const doc::Palette* palette, const doc::PalettePicks& picks);
|
void copyPalette(const doc::Palette* palette, const doc::PalettePicks& picks);
|
||||||
|
void copySlices(const std::vector<doc::Slice*> slices);
|
||||||
void paste(Context* ctx, const bool interactive, const gfx::Point* position = nullptr);
|
void paste(Context* ctx, const bool interactive, const gfx::Point* position = nullptr);
|
||||||
|
|
||||||
doc::ImageRef getImage(doc::Palette* palette);
|
doc::ImageRef getImage(doc::Palette* palette);
|
||||||
|
@ -106,6 +109,7 @@ private:
|
||||||
doc::Mask* mask,
|
doc::Mask* mask,
|
||||||
doc::Palette* palette,
|
doc::Palette* palette,
|
||||||
doc::Tileset* tileset,
|
doc::Tileset* tileset,
|
||||||
|
const std::vector<doc::Slice*>* slices,
|
||||||
bool set_native_clipboard,
|
bool set_native_clipboard,
|
||||||
bool image_source_is_transparent);
|
bool image_source_is_transparent);
|
||||||
bool copyFromDocument(const Site& site, bool merged = false);
|
bool copyFromDocument(const Site& site, bool merged = false);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#include "app/util/slice_utils.h"
|
||||||
|
|
||||||
|
#include "app/context_access.h"
|
||||||
|
#include "app/site.h"
|
||||||
|
#include "doc/slice.h"
|
||||||
|
#include "doc/sprite.h"
|
||||||
|
#include "fmt/format.h"
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
std::string get_unique_slice_name(const doc::Sprite* sprite, const std::string& namePrefix)
|
||||||
|
{
|
||||||
|
std::string prefix = namePrefix.empty() ? "Slice" : namePrefix;
|
||||||
|
int max = 0;
|
||||||
|
|
||||||
|
for (doc::Slice* slice : sprite->slices())
|
||||||
|
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
|
||||||
|
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
|
||||||
|
|
||||||
|
return fmt::format("{} {}", prefix, max + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<doc::Slice*> get_selected_slices(const Site& site)
|
||||||
|
{
|
||||||
|
std::vector<Slice*> selectedSlices;
|
||||||
|
if (site.sprite() && !site.selectedSlices().empty()) {
|
||||||
|
for (auto* slice : site.sprite()->slices()) {
|
||||||
|
if (site.selectedSlices().contains(slice->id())) {
|
||||||
|
selectedSlices.push_back(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedSlices;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Aseprite
|
||||||
|
// Copyright (C) 2025 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This program is distributed under the terms of
|
||||||
|
// the End-User License Agreement for Aseprite.
|
||||||
|
|
||||||
|
#ifndef APP_SLICE_UTILS_H_INCLUDED
|
||||||
|
#define APP_SLICE_UTILS_H_INCLUDED
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace doc {
|
||||||
|
class Slice;
|
||||||
|
class Sprite;
|
||||||
|
} // namespace doc
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
class Site;
|
||||||
|
|
||||||
|
std::string get_unique_slice_name(const doc::Sprite* sprite,
|
||||||
|
const std::string& namePrefix = std::string());
|
||||||
|
|
||||||
|
std::vector<doc::Slice*> get_selected_slices(const Site& site);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif
|
|
@ -9,11 +9,11 @@ if [ $# -ge 2 -a $# -lt 4 ]; then
|
||||||
mkdir -p /tmp/Aseprite
|
mkdir -p /tmp/Aseprite
|
||||||
filename=${1//\//.}$RANDOM
|
filename=${1//\//.}$RANDOM
|
||||||
if [ $# -eq 2 ]; then
|
if [ $# -eq 2 ]; then
|
||||||
aseprite -b --frame-range "0,0" $1 --sheet /tmp/Aseprite/$filename.png
|
aseprite -b --frame-range "0,0" "$1" --sheet "/tmp/Aseprite/$filename.png"
|
||||||
elif [ $# -eq 3 ]; then
|
elif [ $# -eq 3 ]; then
|
||||||
aseprite -b --frame-range "0,0" $1 --shrink-to "$3,$3" --sheet /tmp/Aseprite/$filename.png
|
aseprite -b --frame-range "0,0" "$1" --shrink-to "$3,$3" --sheet "/tmp/Aseprite/$filename.png"
|
||||||
fi
|
fi
|
||||||
mkdir -p $(dirname "$2"); mv /tmp/Aseprite/$filename.png $2;
|
mkdir -p "$(dirname "$2")"; mv "/tmp/Aseprite/$filename.png" "$2";
|
||||||
else
|
else
|
||||||
echo "Parameters for aseprite thumbnailer are: inputfile outputfile [size]"
|
echo "Parameters for aseprite thumbnailer are: inputfile outputfile [size]"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -45,6 +45,7 @@ add_library(doc-lib
|
||||||
image.cpp
|
image.cpp
|
||||||
image_impl.cpp
|
image_impl.cpp
|
||||||
image_io.cpp
|
image_io.cpp
|
||||||
|
image_iterators2.cpp
|
||||||
layer.cpp
|
layer.cpp
|
||||||
layer_io.cpp
|
layer_io.cpp
|
||||||
layer_list.cpp
|
layer_list.cpp
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite Document Library
|
// Aseprite Document Library
|
||||||
// Copyright (c) 2019-2022 Igara Studio S.A.
|
// Copyright (c) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (c) 2001-2017 David Capello
|
// Copyright (c) 2001-2017 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
|
@ -397,73 +397,19 @@ static void set_lum(double& r, double& g, double& b, double l)
|
||||||
clip_color(r, g, b);
|
clip_color(r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline uint8_t get_imin_channel(const double r, const double g, const double b)
|
|
||||||
{
|
|
||||||
// We use '<=' to get min so as to catch two channels being equal
|
|
||||||
if (r <= g && r <= b)
|
|
||||||
return 0b001;
|
|
||||||
if (g <= r && g <= b)
|
|
||||||
return 0b010;
|
|
||||||
|
|
||||||
return 0b100;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline uint8_t get_imax_channel(const double r, const double g, const double b)
|
|
||||||
{
|
|
||||||
if (r > g && r > b)
|
|
||||||
return 0b001;
|
|
||||||
if (g > r && g > b)
|
|
||||||
return 0b010;
|
|
||||||
|
|
||||||
return 0b100;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline uint8_t get_imid_channel(uint8_t imin, uint8_t imax)
|
|
||||||
{
|
|
||||||
// Getting the remaining channel through exclusion guarantees that it is neither min nor max
|
|
||||||
return (~(imax | imin)) & 0b111;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline double& index_to_ref(double& r, double& g, double& b, uint8_t i)
|
|
||||||
{
|
|
||||||
if (i == 0b001)
|
|
||||||
return r;
|
|
||||||
if (i == 0b010)
|
|
||||||
return g;
|
|
||||||
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO replace this with a better impl (and test this, not sure if it's correct)
|
|
||||||
static void set_sat(double& r, double& g, double& b, double s)
|
static void set_sat(double& r, double& g, double& b, double s)
|
||||||
{
|
{
|
||||||
#undef MIN
|
const double minv = std::min(std::min(r, g), b);
|
||||||
#undef MAX
|
const double maxv = std::max(std::max(r, g), b);
|
||||||
#undef MID
|
const double range = maxv - minv;
|
||||||
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
|
|
||||||
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
|
|
||||||
#define MID(x, y, z) \
|
|
||||||
((x) > (y) ? ((y) > (z) ? (y) : ((x) > (z) ? (z) : (x))) : \
|
|
||||||
((y) > (z) ? ((z) > (x) ? (z) : (x)) : (y)))
|
|
||||||
|
|
||||||
// Fetch channel indices
|
if (range > 0.0) {
|
||||||
const uint8_t imin = get_imin_channel(r, g, b);
|
r = ((r - minv) * s) / range;
|
||||||
const uint8_t imax = get_imax_channel(r, g, b);
|
g = ((g - minv) * s) / range;
|
||||||
const uint8_t imid = get_imid_channel(imin, imax);
|
b = ((b - minv) * s) / range;
|
||||||
|
|
||||||
// Map the indices for each channel to references
|
|
||||||
double& min = index_to_ref(r, g, b, imin);
|
|
||||||
double& max = index_to_ref(r, g, b, imax);
|
|
||||||
double& mid = index_to_ref(r, g, b, imid);
|
|
||||||
|
|
||||||
if (max > min) {
|
|
||||||
mid = ((mid - min) * s) / (max - min);
|
|
||||||
max = s;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
mid = max = 0;
|
r = g = b = 0.0;
|
||||||
|
|
||||||
min = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
color_t rgba_blender_hsl_hue(color_t backdrop, color_t src, int opacity)
|
color_t rgba_blender_hsl_hue(color_t backdrop, color_t src, int opacity)
|
||||||
|
@ -552,7 +498,7 @@ color_t rgba_blender_subtract(color_t backdrop, color_t src, int opacity)
|
||||||
int r = rgba_getr(backdrop) - rgba_getr(src);
|
int r = rgba_getr(backdrop) - rgba_getr(src);
|
||||||
int g = rgba_getg(backdrop) - rgba_getg(src);
|
int g = rgba_getg(backdrop) - rgba_getg(src);
|
||||||
int b = rgba_getb(backdrop) - rgba_getb(src);
|
int b = rgba_getb(backdrop) - rgba_getb(src);
|
||||||
src = rgba(MAX(r, 0), MAX(g, 0), MAX(b, 0), 0) | (src & rgba_a_mask);
|
src = rgba(std::max(r, 0), std::max(g, 0), std::max(b, 0), 0) | (src & rgba_a_mask);
|
||||||
return rgba_blender_normal(backdrop, src, opacity);
|
return rgba_blender_normal(backdrop, src, opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite Document Library
|
// Aseprite Document Library
|
||||||
// Copyright (c) 2018-2020 Igara Studio S.A.
|
// Copyright (c) 2018-2024 Igara Studio S.A.
|
||||||
// Copyright (c) 2001-2016 David Capello
|
// Copyright (c) 2001-2016 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite Document Library
|
// Aseprite Document Library
|
||||||
// Copyright (c) 2018-2023 Igara Studio S.A.
|
// Copyright (c) 2018-2024 Igara Studio S.A.
|
||||||
// Copyright (c) 2001-2016 David Capello
|
// Copyright (c) 2001-2016 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
|
@ -12,6 +12,7 @@
|
||||||
#include "doc/color.h"
|
#include "doc/color.h"
|
||||||
#include "doc/color_mode.h"
|
#include "doc/color_mode.h"
|
||||||
#include "doc/image_buffer.h"
|
#include "doc/image_buffer.h"
|
||||||
|
#include "doc/image_iterators2.h"
|
||||||
#include "doc/image_spec.h"
|
#include "doc/image_spec.h"
|
||||||
#include "doc/object.h"
|
#include "doc/object.h"
|
||||||
#include "doc/pixel_format.h"
|
#include "doc/pixel_format.h"
|
||||||
|
@ -103,6 +104,21 @@ public:
|
||||||
virtual void fillRect(int x1, int y1, int x2, int y2, color_t color) = 0;
|
virtual void fillRect(int x1, int y1, int x2, int y2, color_t color) = 0;
|
||||||
virtual void blendRect(int x1, int y1, int x2, int y2, color_t color, int opacity) = 0;
|
virtual void blendRect(int x1, int y1, int x2, int y2, color_t color, int opacity) = 0;
|
||||||
|
|
||||||
|
ReadIterator readArea() const { return ReadIterator(this, this->bounds()); }
|
||||||
|
WriteIterator writeArea() { return WriteIterator(this, this->bounds()); }
|
||||||
|
|
||||||
|
ReadIterator readArea(const gfx::Rect& bounds,
|
||||||
|
const IteratorStart start = IteratorStart::TopLeft) const
|
||||||
|
{
|
||||||
|
return ReadIterator(this, bounds, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteIterator writeArea(const gfx::Rect& bounds,
|
||||||
|
const IteratorStart start = IteratorStart::TopLeft)
|
||||||
|
{
|
||||||
|
return WriteIterator(this, bounds, start);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Image(const ImageSpec& spec);
|
Image(const ImageSpec& spec);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
// Aseprite Document Library
|
||||||
|
// Copyright (c) 2024 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This file is released under the terms of the MIT license.
|
||||||
|
// Read LICENSE.txt for more information.
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "doc/algorithm/random_image.h"
|
||||||
|
#include "doc/image.h"
|
||||||
|
#include "doc/image_ref.h"
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
using namespace doc;
|
||||||
|
|
||||||
|
void BM_ForXYGetPixel(benchmark::State& state)
|
||||||
|
{
|
||||||
|
const auto pf = (PixelFormat)state.range(0);
|
||||||
|
const int w = state.range(1);
|
||||||
|
const int h = state.range(2);
|
||||||
|
ImageRef img(Image::create(pf, w, h));
|
||||||
|
doc::algorithm::random_image(img.get());
|
||||||
|
for (auto _ : state) {
|
||||||
|
color_t c = 0;
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
c += img->getPixel(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BM_ForEachPixel(benchmark::State& state)
|
||||||
|
{
|
||||||
|
const auto pf = (PixelFormat)state.range(0);
|
||||||
|
const int w = state.range(1);
|
||||||
|
const int h = state.range(2);
|
||||||
|
ImageRef img(Image::create(pf, w, h));
|
||||||
|
doc::algorithm::random_image(img.get());
|
||||||
|
for (auto _ : state) {
|
||||||
|
color_t c = 0;
|
||||||
|
auto func = [&c](color_t u) { c += u; };
|
||||||
|
switch (pf) {
|
||||||
|
case IMAGE_RGB: for_each_pixel<RgbTraits>(img.get(), func); break;
|
||||||
|
case IMAGE_GRAYSCALE: for_each_pixel<GrayscaleTraits>(img.get(), func); break;
|
||||||
|
case IMAGE_INDEXED: for_each_pixel<IndexedTraits>(img.get(), func); break;
|
||||||
|
case IMAGE_BITMAP: for_each_pixel<BitmapTraits>(img.get(), func); break;
|
||||||
|
case IMAGE_TILEMAP: for_each_pixel<TilemapTraits>(img.get(), func); break;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BM_ForXYGetPixelFast(benchmark::State& state)
|
||||||
|
{
|
||||||
|
const auto pf = (PixelFormat)state.range(0);
|
||||||
|
const int w = state.range(1);
|
||||||
|
const int h = state.range(2);
|
||||||
|
ImageRef img(Image::create(pf, w, h));
|
||||||
|
doc::algorithm::random_image(img.get());
|
||||||
|
for (auto _ : state) {
|
||||||
|
color_t c = 0;
|
||||||
|
switch (pf) {
|
||||||
|
case IMAGE_RGB:
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
c += get_pixel_fast<RgbTraits>(img.get(), x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IMAGE_GRAYSCALE:
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
c += get_pixel_fast<GrayscaleTraits>(img.get(), x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IMAGE_INDEXED:
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
c += get_pixel_fast<IndexedTraits>(img.get(), x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IMAGE_BITMAP:
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
c += get_pixel_fast<BitmapTraits>(img.get(), x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Func>
|
||||||
|
void dispatch_by_pixel_format(Image* image, const ReadIterator& it, Func func)
|
||||||
|
{
|
||||||
|
switch (image->pixelFormat()) {
|
||||||
|
case IMAGE_RGB: func(it.addr32()); break;
|
||||||
|
case IMAGE_GRAYSCALE: func(it.addr16()); break;
|
||||||
|
case IMAGE_INDEXED: func(it.addr8()); break;
|
||||||
|
#if !DOC_USE_BITMAP_AS_1BPP
|
||||||
|
case IMAGE_BITMAP: func(it.addr8()); break;
|
||||||
|
#endif
|
||||||
|
case IMAGE_TILEMAP: func(it.addr32()); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BM_ReadIterator(benchmark::State& state)
|
||||||
|
{
|
||||||
|
const auto pf = (PixelFormat)state.range(0);
|
||||||
|
const int w = state.range(1);
|
||||||
|
const int h = state.range(2);
|
||||||
|
ImageRef img(Image::create(pf, w, h));
|
||||||
|
doc::algorithm::random_image(img.get());
|
||||||
|
for (auto _ : state) {
|
||||||
|
auto it = img->readArea(img->bounds());
|
||||||
|
while (it.nextLine()) {
|
||||||
|
dispatch_by_pixel_format(img.get(), it, [w](auto addr) {
|
||||||
|
color_t c = 0;
|
||||||
|
for (int x = 0; x < w; ++x, ++addr) {
|
||||||
|
c += *addr;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DEFARGS() \
|
||||||
|
->Args({ IMAGE_RGB, 1024, 1024 }) \
|
||||||
|
->Args({ IMAGE_GRAYSCALE, 1024, 1024 }) \
|
||||||
|
->Args({ IMAGE_INDEXED, 1024, 1024 }) \
|
||||||
|
->Args({ IMAGE_BITMAP, 1024, 1024 })
|
||||||
|
|
||||||
|
#if DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
|
// In this case we avoid IMAGE_BITMAP as its iterator goes through
|
||||||
|
// bits instead of bytes, i.e. a pixel cannot be addressed with a
|
||||||
|
// memory pointer.
|
||||||
|
#define DEFARGS_ADDRESSABLE_ONLY() \
|
||||||
|
->Args({ IMAGE_RGB, 1024, 1024 }) \
|
||||||
|
->Args({ IMAGE_GRAYSCALE, 1024, 1024 }) \
|
||||||
|
->Args({ IMAGE_INDEXED, 1024, 1024 })
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#define DEFARGS_ADDRESSABLE_ONLY DEFARGS
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
BENCHMARK(BM_ForXYGetPixel)
|
||||||
|
DEFARGS()->UseRealTime();
|
||||||
|
|
||||||
|
BENCHMARK(BM_ForEachPixel)
|
||||||
|
DEFARGS()->UseRealTime();
|
||||||
|
|
||||||
|
BENCHMARK(BM_ForXYGetPixelFast)
|
||||||
|
DEFARGS()->UseRealTime();
|
||||||
|
|
||||||
|
BENCHMARK(BM_ReadIterator)
|
||||||
|
DEFARGS_ADDRESSABLE_ONLY()->UseRealTime();
|
||||||
|
|
||||||
|
BENCHMARK_MAIN();
|
|
@ -223,6 +223,26 @@ private:
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Specializations
|
// Specializations
|
||||||
|
|
||||||
|
template<>
|
||||||
|
inline void ImageImpl<RgbTraits>::blendRect(int x1,
|
||||||
|
int y1,
|
||||||
|
int x2,
|
||||||
|
int y2,
|
||||||
|
color_t color,
|
||||||
|
int opacity)
|
||||||
|
{
|
||||||
|
address_t addr;
|
||||||
|
int x, y;
|
||||||
|
|
||||||
|
for (y = y1; y <= y2; ++y) {
|
||||||
|
addr = (address_t)getPixelAddress(x1, y);
|
||||||
|
for (x = x1; x <= x2; ++x) {
|
||||||
|
*addr = rgba_blender_normal(*addr, color, opacity);
|
||||||
|
++addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
inline void ImageImpl<IndexedTraits>::clear(color_t color)
|
inline void ImageImpl<IndexedTraits>::clear(color_t color)
|
||||||
{
|
{
|
||||||
|
@ -237,6 +257,8 @@ inline void ImageImpl<BitmapTraits>::clear(color_t color)
|
||||||
std::fill(p, p + rowBytes() * height(), (color ? 0xff : 0x00));
|
std::fill(p, p + rowBytes() * height(), (color ? 0xff : 0x00));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
inline color_t ImageImpl<BitmapTraits>::getPixel(int x, int y) const
|
inline color_t ImageImpl<BitmapTraits>::getPixel(int x, int y) const
|
||||||
{
|
{
|
||||||
|
@ -267,26 +289,6 @@ inline void ImageImpl<BitmapTraits>::fillRect(int x1, int y1, int x2, int y2, co
|
||||||
ImageImpl<BitmapTraits>::drawHLine(x1, y, x2, color);
|
ImageImpl<BitmapTraits>::drawHLine(x1, y, x2, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
|
||||||
inline void ImageImpl<RgbTraits>::blendRect(int x1,
|
|
||||||
int y1,
|
|
||||||
int x2,
|
|
||||||
int y2,
|
|
||||||
color_t color,
|
|
||||||
int opacity)
|
|
||||||
{
|
|
||||||
address_t addr;
|
|
||||||
int x, y;
|
|
||||||
|
|
||||||
for (y = y1; y <= y2; ++y) {
|
|
||||||
addr = (address_t)getPixelAddress(x1, y);
|
|
||||||
for (x = x1; x <= x2; ++x) {
|
|
||||||
*addr = rgba_blender_normal(*addr, color, opacity);
|
|
||||||
++addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void copy_bitmaps(Image* dst, const Image* src, gfx::Clip area);
|
void copy_bitmaps(Image* dst, const Image* src, gfx::Clip area);
|
||||||
template<>
|
template<>
|
||||||
inline void ImageImpl<BitmapTraits>::copy(const Image* src, gfx::Clip area)
|
inline void ImageImpl<BitmapTraits>::copy(const Image* src, gfx::Clip area)
|
||||||
|
@ -294,6 +296,8 @@ inline void ImageImpl<BitmapTraits>::copy(const Image* src, gfx::Clip area)
|
||||||
copy_bitmaps(this, src, area);
|
copy_bitmaps(this, src, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
} // namespace doc
|
} // namespace doc
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -170,7 +170,9 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Iterator for BitmapTraits
|
// Iterator for BitmapTraits when it's 1bpp
|
||||||
|
|
||||||
|
#if DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
class BitPixelAccess {
|
class BitPixelAccess {
|
||||||
public:
|
public:
|
||||||
|
@ -403,6 +405,8 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endif // DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
} // namespace doc
|
} // namespace doc
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Aseprite Document Library
|
||||||
|
// Copyright (c) 2024 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This file is released under the terms of the MIT license.
|
||||||
|
// Read LICENSE.txt for more information.
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "doc/image.h"
|
||||||
|
|
||||||
|
namespace doc {
|
||||||
|
|
||||||
|
ReadIterator::ReadIterator(const Image* image, const gfx::Rect& bounds, const IteratorStart start)
|
||||||
|
: m_rows(bounds.h)
|
||||||
|
{
|
||||||
|
switch (start) {
|
||||||
|
case IteratorStart::TopLeft:
|
||||||
|
m_addr = image->getPixelAddress(bounds.x, bounds.y);
|
||||||
|
m_nextRow = image->rowBytes();
|
||||||
|
break;
|
||||||
|
case IteratorStart::TopRight:
|
||||||
|
m_addr = image->getPixelAddress(bounds.x2() - 1, bounds.y);
|
||||||
|
m_nextRow = image->rowBytes();
|
||||||
|
break;
|
||||||
|
case IteratorStart::BottomLeft:
|
||||||
|
m_addr = image->getPixelAddress(bounds.x, bounds.y2() - 1);
|
||||||
|
m_nextRow = -image->rowBytes();
|
||||||
|
break;
|
||||||
|
case IteratorStart::BottomRight:
|
||||||
|
m_addr = image->getPixelAddress(bounds.x2() - 1, bounds.y2() - 1);
|
||||||
|
m_nextRow = -image->rowBytes();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_addr -= m_nextRow; // This is canceled in the first nextLine() call.
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace doc
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Aseprite Document Library
|
||||||
|
// Copyright (c) 2024 Igara Studio S.A.
|
||||||
|
//
|
||||||
|
// This file is released under the terms of the MIT license.
|
||||||
|
// Read LICENSE.txt for more information.
|
||||||
|
|
||||||
|
#ifndef DOC_IMAGE_ITERATOR2_H_INCLUDED
|
||||||
|
#define DOC_IMAGE_ITERATOR2_H_INCLUDED
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base/ints.h"
|
||||||
|
#include "gfx/fwd.h"
|
||||||
|
|
||||||
|
namespace doc {
|
||||||
|
|
||||||
|
class Image;
|
||||||
|
|
||||||
|
// New iterators classes.
|
||||||
|
|
||||||
|
enum class IteratorStart : uint8_t { TopLeft, TopRight, BottomLeft, BottomRight };
|
||||||
|
|
||||||
|
class ReadIterator {
|
||||||
|
public:
|
||||||
|
ReadIterator(const Image* image,
|
||||||
|
const gfx::Rect& bounds,
|
||||||
|
IteratorStart start = IteratorStart::TopLeft);
|
||||||
|
|
||||||
|
const uint8_t* addr8() const { return m_addr; }
|
||||||
|
const uint16_t* addr16() const { return (uint16_t*)m_addr; }
|
||||||
|
const uint32_t* addr32() const { return (uint32_t*)m_addr; }
|
||||||
|
|
||||||
|
template<typename ImageTraits>
|
||||||
|
typename ImageTraits::const_address_t addr() const
|
||||||
|
{
|
||||||
|
return (typename ImageTraits::const_address_t)m_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nextLine()
|
||||||
|
{
|
||||||
|
m_addr += m_nextRow;
|
||||||
|
return (m_rows-- > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint8_t* m_addr = nullptr;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_rows = 0;
|
||||||
|
int m_nextRow = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WriteIterator : public ReadIterator {
|
||||||
|
public:
|
||||||
|
WriteIterator(Image* image,
|
||||||
|
const gfx::Rect& bounds,
|
||||||
|
const IteratorStart start = IteratorStart::TopLeft)
|
||||||
|
: ReadIterator(image, bounds, start)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* addr8() { return m_addr; }
|
||||||
|
uint16_t* addr16() { return (uint16_t*)m_addr; }
|
||||||
|
uint32_t* addr32() { return (uint32_t*)m_addr; }
|
||||||
|
|
||||||
|
template<typename ImageTraits>
|
||||||
|
uint32_t* addr()
|
||||||
|
{
|
||||||
|
return (typename ImageTraits::address_t)m_addr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace doc
|
||||||
|
|
||||||
|
#endif
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "doc/algorithm/random_image.h"
|
||||||
#include "doc/image.h"
|
#include "doc/image.h"
|
||||||
#include "doc/primitives.h"
|
#include "doc/primitives.h"
|
||||||
|
|
||||||
|
@ -25,9 +26,27 @@ protected:
|
||||||
ImageAllTypes() {}
|
ImageAllTypes() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits, BitmapTraits> ImageAllTraits;
|
using ImageAllTraits = testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits, BitmapTraits>;
|
||||||
|
|
||||||
TYPED_TEST_SUITE(ImageAllTypes, ImageAllTraits);
|
TYPED_TEST_SUITE(ImageAllTypes, ImageAllTraits);
|
||||||
|
|
||||||
|
#if DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
class ImageAllTypesNoBitmap : public testing::Test {
|
||||||
|
protected:
|
||||||
|
ImageAllTypesNoBitmap() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
using ImageAllTraitsNoBitmap = testing::Types<RgbTraits, GrayscaleTraits, IndexedTraits>;
|
||||||
|
TYPED_TEST_SUITE(ImageAllTypesNoBitmap, ImageAllTraitsNoBitmap);
|
||||||
|
|
||||||
|
#else // !DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
|
#define ImageAllTypesNoBitmap ImageAllTypes
|
||||||
|
|
||||||
|
#endif // !DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
TYPED_TEST(ImageAllTypes, PutGetAndIterators)
|
TYPED_TEST(ImageAllTypes, PutGetAndIterators)
|
||||||
{
|
{
|
||||||
using ImageTraits = TypeParam;
|
using ImageTraits = TypeParam;
|
||||||
|
@ -195,6 +214,75 @@ TYPED_TEST(ImageAllTypes, FillRect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TYPED_TEST(ImageAllTypesNoBitmap, NewIterators)
|
||||||
|
{
|
||||||
|
using ImageTraits = TypeParam;
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; ++i) {
|
||||||
|
const int w = 1 + i;
|
||||||
|
const int h = 1 + i;
|
||||||
|
|
||||||
|
std::unique_ptr<Image> image(Image::create(ImageTraits::pixel_format, w, h));
|
||||||
|
doc::algorithm::random_image(image.get());
|
||||||
|
|
||||||
|
// TopLeft
|
||||||
|
{
|
||||||
|
int v = 0;
|
||||||
|
auto it = image->readArea(image->bounds(), IteratorStart::TopLeft);
|
||||||
|
while (it.nextLine()) {
|
||||||
|
auto* addr = (typename ImageTraits::address_t)it.addr8();
|
||||||
|
for (int u = 0; u < w; ++u, ++addr) {
|
||||||
|
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
|
||||||
|
ASSERT_EQ(expected, *addr);
|
||||||
|
}
|
||||||
|
++v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopRight
|
||||||
|
{
|
||||||
|
int v = 0;
|
||||||
|
auto it = image->readArea(image->bounds(), IteratorStart::TopRight);
|
||||||
|
while (it.nextLine()) {
|
||||||
|
auto* addr = (typename ImageTraits::address_t)it.addr8();
|
||||||
|
for (int u = w - 1; u >= 0; --u, --addr) {
|
||||||
|
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
|
||||||
|
ASSERT_EQ(expected, *addr);
|
||||||
|
}
|
||||||
|
++v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BottomLeft
|
||||||
|
{
|
||||||
|
int v = h - 1;
|
||||||
|
auto it = image->readArea(image->bounds(), IteratorStart::BottomLeft);
|
||||||
|
while (it.nextLine()) {
|
||||||
|
auto* addr = (typename ImageTraits::address_t)it.addr8();
|
||||||
|
for (int u = 0; u < w; ++u, ++addr) {
|
||||||
|
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
|
||||||
|
ASSERT_EQ(expected, *addr);
|
||||||
|
}
|
||||||
|
--v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BottomRight
|
||||||
|
{
|
||||||
|
int v = h - 1;
|
||||||
|
auto it = image->readArea(image->bounds(), IteratorStart::BottomRight);
|
||||||
|
while (it.nextLine()) {
|
||||||
|
auto* addr = (typename ImageTraits::address_t)it.addr8();
|
||||||
|
for (int u = w - 1; u >= 0; --u, --addr) {
|
||||||
|
auto expected = get_pixel_fast<ImageTraits>(image.get(), u, v);
|
||||||
|
ASSERT_EQ(expected, *addr);
|
||||||
|
}
|
||||||
|
--v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
|
|
@ -140,6 +140,8 @@ struct IndexedTraits {
|
||||||
static inline bool same_color(const pixel_t a, const pixel_t b) { return a == b; }
|
static inline bool same_color(const pixel_t a, const pixel_t b) { return a == b; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
struct BitmapTraits {
|
struct BitmapTraits {
|
||||||
static const ColorMode color_mode = ColorMode::BITMAP;
|
static const ColorMode color_mode = ColorMode::BITMAP;
|
||||||
static const PixelFormat pixel_format = IMAGE_BITMAP;
|
static const PixelFormat pixel_format = IMAGE_BITMAP;
|
||||||
|
@ -169,6 +171,47 @@ struct BitmapTraits {
|
||||||
static inline bool same_color(const pixel_t a, const pixel_t b) { return a == b; }
|
static inline bool same_color(const pixel_t a, const pixel_t b) { return a == b; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#else // !DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
|
struct BitmapTraits {
|
||||||
|
static const ColorMode color_mode = ColorMode::BITMAP;
|
||||||
|
static const PixelFormat pixel_format = IMAGE_BITMAP;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
bits_per_pixel = 8,
|
||||||
|
bytes_per_pixel = 1,
|
||||||
|
pixels_per_byte = 1,
|
||||||
|
channels = 1,
|
||||||
|
has_alpha = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef uint8_t pixel_t;
|
||||||
|
typedef pixel_t* address_t;
|
||||||
|
typedef const pixel_t* const_address_t;
|
||||||
|
|
||||||
|
static const pixel_t min_value = 0x00;
|
||||||
|
static const pixel_t max_value = 0xff;
|
||||||
|
|
||||||
|
static inline int width_bytes(int pixels_per_row) { return bytes_per_pixel * pixels_per_row; }
|
||||||
|
|
||||||
|
static inline int rowstride_bytes(int pixels_per_row)
|
||||||
|
{
|
||||||
|
return doc_align_size(width_bytes(pixels_per_row));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline BlendFunc get_blender(BlendMode blend_mode, bool newBlend)
|
||||||
|
{
|
||||||
|
return get_indexed_blender(blend_mode, newBlend);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool same_color(const pixel_t a, const pixel_t b)
|
||||||
|
{
|
||||||
|
return (a == 0 && b == 0) || (a != 0 && b != 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // !DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
struct TilemapTraits {
|
struct TilemapTraits {
|
||||||
static const ColorMode color_mode = ColorMode::TILEMAP;
|
static const ColorMode color_mode = ColorMode::TILEMAP;
|
||||||
static const PixelFormat pixel_format = IMAGE_TILEMAP;
|
static const PixelFormat pixel_format = IMAGE_TILEMAP;
|
||||||
|
|
|
@ -597,4 +597,22 @@ void LayerGroup::displaceFrames(frame_t fromThis, frame_t delta)
|
||||||
layer->displaceFrames(fromThis, delta);
|
layer->displaceFrames(fromThis, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layer_t LayerGroup::getLayerIndex(const Layer* layer, layer_t& index) const
|
||||||
|
{
|
||||||
|
for (Layer* child : this->layers()) {
|
||||||
|
if ((child->isGroup() && static_cast<LayerGroup*>(child)->getLayerIndex(layer, index) != -1) ||
|
||||||
|
(child == layer)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer_t LayerGroup::getLayerIndex(const Layer* layer) const
|
||||||
|
{
|
||||||
|
layer_t index = 0;
|
||||||
|
return this->getLayerIndex(layer, index);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace doc
|
} // namespace doc
|
||||||
|
|
|
@ -236,9 +236,13 @@ public:
|
||||||
|
|
||||||
bool isBrowsable() const override { return isGroup() && isExpanded() && !m_layers.empty(); }
|
bool isBrowsable() const override { return isGroup() && isExpanded() && !m_layers.empty(); }
|
||||||
|
|
||||||
|
layer_t getLayerIndex(const Layer* layer) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void destroyAllLayers();
|
void destroyAllLayers();
|
||||||
|
|
||||||
|
layer_t getLayerIndex(const Layer* layer, layer_t& index) const;
|
||||||
|
|
||||||
LayerList m_layers;
|
LayerList m_layers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite Document Library
|
// Aseprite Document Library
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
// Copyright (C) 2001-2018 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
|
@ -46,24 +46,16 @@ void for_each_mask_pixel(Mask& a, const Mask& b, Func f)
|
||||||
|
|
||||||
Mask::Mask() : Object(ObjectType::Mask)
|
Mask::Mask() : Object(ObjectType::Mask)
|
||||||
{
|
{
|
||||||
initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mask::Mask(const Mask& mask) : Object(mask)
|
Mask::Mask(const Mask& mask) : Object(mask)
|
||||||
{
|
{
|
||||||
initialize();
|
|
||||||
copyFrom(&mask);
|
copyFrom(&mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
Mask::~Mask()
|
Mask::~Mask()
|
||||||
{
|
{
|
||||||
ASSERT(m_freeze_count == 0);
|
ASSERT(m_freezes == 0);
|
||||||
}
|
|
||||||
|
|
||||||
void Mask::initialize()
|
|
||||||
{
|
|
||||||
m_freeze_count = 0;
|
|
||||||
m_bounds = gfx::Rect(0, 0, 0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int Mask::getMemSize() const
|
int Mask::getMemSize() const
|
||||||
|
@ -78,17 +70,17 @@ void Mask::setName(const char* name)
|
||||||
|
|
||||||
void Mask::freeze()
|
void Mask::freeze()
|
||||||
{
|
{
|
||||||
ASSERT(m_freeze_count >= 0);
|
ASSERT(m_freezes >= 0);
|
||||||
m_freeze_count++;
|
m_freezes++;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mask::unfreeze()
|
void Mask::unfreeze()
|
||||||
{
|
{
|
||||||
ASSERT(m_freeze_count > 0);
|
ASSERT(m_freezes > 0);
|
||||||
m_freeze_count--;
|
m_freezes--;
|
||||||
|
|
||||||
// Shrink just in case
|
// Shrink just in case
|
||||||
if (m_freeze_count == 0)
|
if (m_freezes == 0)
|
||||||
shrink();
|
shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +102,7 @@ bool Mask::isRectangular() const
|
||||||
|
|
||||||
void Mask::copyFrom(const Mask* sourceMask)
|
void Mask::copyFrom(const Mask* sourceMask)
|
||||||
{
|
{
|
||||||
ASSERT(m_freeze_count == 0);
|
ASSERT(m_freezes == 0);
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
setName(sourceMask->name().c_str());
|
setName(sourceMask->name().c_str());
|
||||||
|
@ -245,10 +237,10 @@ void Mask::intersect(const doc::Mask& mask)
|
||||||
|
|
||||||
void Mask::add(const gfx::Rect& bounds)
|
void Mask::add(const gfx::Rect& bounds)
|
||||||
{
|
{
|
||||||
if (m_freeze_count == 0)
|
if (m_freezes == 0)
|
||||||
reserve(bounds);
|
reserve(bounds);
|
||||||
|
|
||||||
// m_bitmap can be nullptr if we have m_freeze_count > 0
|
// m_bitmap can be nullptr if we have m_freezes > 0
|
||||||
if (!m_bitmap)
|
if (!m_bitmap)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -490,7 +482,7 @@ void Mask::reserve(const gfx::Rect& bounds)
|
||||||
void Mask::shrink()
|
void Mask::shrink()
|
||||||
{
|
{
|
||||||
// If the mask is frozen we avoid the shrinking
|
// If the mask is frozen we avoid the shrinking
|
||||||
if (m_freeze_count > 0)
|
if (m_freezes > 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#define SHRINK_SIDE(u_begin, u_op, u_final, u_add, v_begin, v_op, v_final, v_add, U, V, var) \
|
#define SHRINK_SIDE(u_begin, u_op, u_final, u_add, v_begin, v_op, v_final, v_add, U, V, var) \
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite Document Library
|
// Aseprite Document Library
|
||||||
// Copyright (c) 2020-2024 Igara Studio S.A.
|
// Copyright (c) 2020-2025 Igara Studio S.A.
|
||||||
// Copyright (c) 2001-2018 David Capello
|
// Copyright (c) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
|
@ -65,7 +65,7 @@ public:
|
||||||
void unfreeze();
|
void unfreeze();
|
||||||
|
|
||||||
// Returns true if the mask is frozen (See freeze/unfreeze functions).
|
// Returns true if the mask is frozen (See freeze/unfreeze functions).
|
||||||
bool isFrozen() const { return m_freeze_count > 0; }
|
bool isFrozen() const { return m_freezes > 0; }
|
||||||
|
|
||||||
// Returns true if the mask is a rectangular region.
|
// Returns true if the mask is a rectangular region.
|
||||||
bool isRectangular() const;
|
bool isRectangular() const;
|
||||||
|
@ -107,9 +107,7 @@ public:
|
||||||
void offsetOrigin(int dx, int dy);
|
void offsetOrigin(int dx, int dy);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initialize();
|
int m_freezes = 0;
|
||||||
|
|
||||||
int m_freeze_count;
|
|
||||||
std::string m_name; // Mask name
|
std::string m_name; // Mask name
|
||||||
gfx::Rect m_bounds; // Region bounds
|
gfx::Rect m_bounds; // Region bounds
|
||||||
ImageRef m_bitmap; // Bitmapped image mask
|
ImageRef m_bitmap; // Bitmapped image mask
|
||||||
|
|
|
@ -9,6 +9,12 @@
|
||||||
#define DOC_PIXEL_FORMAT_H_INCLUDED
|
#define DOC_PIXEL_FORMAT_H_INCLUDED
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
// Use 1-bit per pixel in IMAGE_BITMAP
|
||||||
|
#define DOC_USE_BITMAP_AS_1BPP 1
|
||||||
|
|
||||||
|
// Use 8-bit per pixel in IMAGE_BITMAP
|
||||||
|
// #define DOC_USE_BITMAP_AS_1BPP 0
|
||||||
|
|
||||||
namespace doc {
|
namespace doc {
|
||||||
|
|
||||||
// This enum might be replaced/deprecated/removed in the future, please use
|
// This enum might be replaced/deprecated/removed in the future, please use
|
||||||
|
|
|
@ -49,6 +49,8 @@ inline void put_pixel_fast(Image* image, int x, int y, typename Traits::pixel_t
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Bitmap specialization
|
// Bitmap specialization
|
||||||
|
|
||||||
|
#if DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
inline BitmapTraits::pixel_t get_pixel_fast<BitmapTraits>(const Image* image, int x, int y)
|
inline BitmapTraits::pixel_t get_pixel_fast<BitmapTraits>(const Image* image, int x, int y)
|
||||||
{
|
{
|
||||||
|
@ -70,6 +72,8 @@ inline void put_pixel_fast<BitmapTraits>(Image* image, int x, int y, BitmapTrait
|
||||||
*image->getPixelAddress(x, y) &= ~(1 << (x % 8));
|
*image->getPixelAddress(x, y) &= ~(1 << (x % 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // DOC_USE_BITMAP_AS_1BPP
|
||||||
|
|
||||||
} // namespace doc
|
} // namespace doc
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -31,7 +31,10 @@ Slices::~Slices()
|
||||||
|
|
||||||
void Slices::add(Slice* slice)
|
void Slices::add(Slice* slice)
|
||||||
{
|
{
|
||||||
m_slices.push_back(slice);
|
// Insert the slice at the begining to display it at the front of the others.
|
||||||
|
// This is useful when duplicating (or copy & pasting) slices, because the
|
||||||
|
// user can drag the new slices instead of the originally selected ones.
|
||||||
|
m_slices.insert(m_slices.begin(), slice);
|
||||||
slice->setOwner(this);
|
slice->setOwner(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>aseprite</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>Document.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Aseprite Sprite</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>ase</string>
|
||||||
|
<string>bmp</string>
|
||||||
|
<string>flc</string>
|
||||||
|
<string>fli</string>
|
||||||
|
<string>gif</string>
|
||||||
|
<string>ico</string>
|
||||||
|
<string>jpeg</string>
|
||||||
|
<string>jpg</string>
|
||||||
|
<string>pcx</string>
|
||||||
|
<string>png</string>
|
||||||
|
<string>tga</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>Document.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Aseprite Sprite</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>aseprite-extension</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>Extension.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Aseprite Extension</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Aseprite</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>aseprite</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.aseprite.Aseprite</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Aseprite</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.3</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.3</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>Aseprite.icns</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.graphics-design</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright © 2001-2025, Igara Studio S.A.
|
||||||
|
All rights reserved.</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSRequiresAquaSystemAppearance</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -1,5 +1,5 @@
|
||||||
// Aseprite UI Library
|
// Aseprite UI Library
|
||||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||||
//
|
//
|
||||||
// This file is released under the terms of the MIT license.
|
// This file is released under the terms of the MIT license.
|
||||||
// Read LICENSE.txt for more information.
|
// Read LICENSE.txt for more information.
|
||||||
|
@ -87,7 +87,8 @@ void Display::configureBackLayer()
|
||||||
layerSurface->width() != displaySurface->width() ||
|
layerSurface->width() != displaySurface->width() ||
|
||||||
layerSurface->height() != displaySurface->height()) {
|
layerSurface->height() != displaySurface->height()) {
|
||||||
layerSurface = os::System::instance()->makeSurface(displaySurface->width(),
|
layerSurface = os::System::instance()->makeSurface(displaySurface->width(),
|
||||||
displaySurface->height());
|
displaySurface->height(),
|
||||||
|
displaySurface->colorSpace());
|
||||||
layer->setSurface(layerSurface);
|
layer->setSurface(layerSurface);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,15 @@ int Entry::lastCaretPos() const
|
||||||
return int(m_boxes.size() - 1);
|
return int(m_boxes.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gfx::Point Entry::caretPosOnScreen() const
|
||||||
|
{
|
||||||
|
const gfx::Point caretPos = getCharBoxBounds(m_caret).point2();
|
||||||
|
const os::Window* nativeWindow = display()->nativeWindow();
|
||||||
|
const gfx::Point pos = nativeWindow->pointToScreen(caretPos + bounds().origin());
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
void Entry::setCaretPos(const int pos)
|
void Entry::setCaretPos(const int pos)
|
||||||
{
|
{
|
||||||
gfx::Size caretSize = theme()->getEntryCaretSize(this);
|
gfx::Size caretSize = theme()->getEntryCaretSize(this);
|
||||||
|
@ -160,6 +169,8 @@ void Entry::setCaretPos(const int pos)
|
||||||
startTimer();
|
startTimer();
|
||||||
m_state = true;
|
m_state = true;
|
||||||
|
|
||||||
|
os::System::instance()->setTextInput(true, caretPosOnScreen());
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +262,7 @@ gfx::Rect Entry::getEntryTextBounds() const
|
||||||
return onGetEntryTextBounds();
|
return onGetEntryTextBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::Rect Entry::getCharBoxBounds(const int i)
|
gfx::Rect Entry::getCharBoxBounds(const int i) const
|
||||||
{
|
{
|
||||||
ASSERT(i >= 0 && i < int(m_boxes.size()));
|
ASSERT(i >= 0 && i < int(m_boxes.size()));
|
||||||
if (i >= 0 && i < int(m_boxes.size()))
|
if (i >= 0 && i < int(m_boxes.size()))
|
||||||
|
@ -288,8 +299,9 @@ bool Entry::onProcessMessage(Message* msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start processing dead keys
|
// Start processing dead keys
|
||||||
if (m_translate_dead_keys)
|
if (m_translate_dead_keys) {
|
||||||
os::System::instance()->setTextInput(true);
|
os::System::instance()->setTextInput(true, caretPosOnScreen());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case kFocusLeaveMessage:
|
case kFocusLeaveMessage:
|
||||||
|
|
|
@ -43,6 +43,7 @@ public:
|
||||||
|
|
||||||
int caretPos() const { return m_caret; }
|
int caretPos() const { return m_caret; }
|
||||||
int lastCaretPos() const;
|
int lastCaretPos() const;
|
||||||
|
gfx::Point caretPosOnScreen() const;
|
||||||
|
|
||||||
void setCaretPos(int pos);
|
void setCaretPos(int pos);
|
||||||
void setCaretToEnd();
|
void setCaretToEnd();
|
||||||
|
@ -76,7 +77,7 @@ public:
|
||||||
obs::signal<void()> Change;
|
obs::signal<void()> Change;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
gfx::Rect getCharBoxBounds(int i);
|
gfx::Rect getCharBoxBounds(int i) const;
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
bool onProcessMessage(Message* msg) override;
|
bool onProcessMessage(Message* msg) override;
|
||||||
|
|
|
@ -936,6 +936,10 @@ void Widget::getDrawableRegion(gfx::Region& region, DrawableRegionFlags flags)
|
||||||
if (p) {
|
if (p) {
|
||||||
region &= Region(p->bounds());
|
region &= Region(p->bounds());
|
||||||
}
|
}
|
||||||
|
// Intersect with window bounds
|
||||||
|
if (this->window()) {
|
||||||
|
region &= Region(this->window()->bounds());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit to the displayable area
|
// Limit to the displayable area
|
||||||
|
|
|
@ -140,6 +140,76 @@ do -- NewLayer/RemoveLayer
|
||||||
assert(#s.layers == 1)
|
assert(#s.layers == 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- NewLayer Ordering
|
||||||
|
do
|
||||||
|
local function testNewLayerOrdering(testGroup, testTilemap)
|
||||||
|
local s = Sprite(32, 32)
|
||||||
|
assert(#s.layers == 1)
|
||||||
|
local base = app.layer
|
||||||
|
|
||||||
|
app.command.NewLayer{ name = "First", group = testGroup, tilemap = testTilemap }
|
||||||
|
local first = app.layer
|
||||||
|
app.command.NewLayer{ name = "Second", group = testGroup, tilemap = testTilemap }
|
||||||
|
local second = app.layer
|
||||||
|
app.command.NewLayer{ name = "Third", group = testGroup, tilemap = testTilemap }
|
||||||
|
local third = app.layer
|
||||||
|
|
||||||
|
s:deleteLayer(base)
|
||||||
|
|
||||||
|
expect_eq(first.name, "First")
|
||||||
|
expect_eq(second.name, "Second")
|
||||||
|
expect_eq(third.name, "Third")
|
||||||
|
|
||||||
|
expect_eq(first.stackIndex, 1)
|
||||||
|
expect_eq(second.stackIndex, 2)
|
||||||
|
expect_eq(third.stackIndex, 3)
|
||||||
|
|
||||||
|
app.layer = second
|
||||||
|
app.command.NewLayer{ before = true, name = "Before Second", group = testGroup, tilemap = testTilemap }
|
||||||
|
local beforeSecond = app.layer
|
||||||
|
|
||||||
|
app.layer = second
|
||||||
|
app.command.NewLayer{ before = false, name = "After Second", group = testGroup, tilemap = testTilemap }
|
||||||
|
local afterSecond = app.layer
|
||||||
|
|
||||||
|
expect_eq(first.name, "First")
|
||||||
|
expect_eq(second.name, "Second")
|
||||||
|
expect_eq(third.name, "Third")
|
||||||
|
expect_eq(beforeSecond.name, "Before Second")
|
||||||
|
expect_eq(afterSecond.name, "After Second")
|
||||||
|
|
||||||
|
expect_eq(first.stackIndex, 1)
|
||||||
|
expect_eq(beforeSecond.stackIndex, 2)
|
||||||
|
expect_eq(second.stackIndex, 3)
|
||||||
|
expect_eq(afterSecond.stackIndex, 4)
|
||||||
|
expect_eq(third.stackIndex, 5)
|
||||||
|
|
||||||
|
app.layer = second
|
||||||
|
app.command.NewLayer{ top = true, name = "Top", group = testGroup, tilemap = testTilemap }
|
||||||
|
local top = app.layer
|
||||||
|
|
||||||
|
expect_eq(top.stackIndex, 6)
|
||||||
|
|
||||||
|
app.layer = first
|
||||||
|
app.command.NewLayer{ before = true, name = "Bottom", group = testGroup, tilemap = testTilemap }
|
||||||
|
local bottom = app.layer
|
||||||
|
|
||||||
|
expect_eq(bottom.stackIndex, 1)
|
||||||
|
expect_eq(first.stackIndex, 2)
|
||||||
|
expect_eq(beforeSecond.stackIndex, 3)
|
||||||
|
expect_eq(second.stackIndex, 4)
|
||||||
|
expect_eq(afterSecond.stackIndex, 5)
|
||||||
|
expect_eq(third.stackIndex, 6)
|
||||||
|
expect_eq(top.stackIndex, 7)
|
||||||
|
|
||||||
|
s:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
testNewLayerOrdering(false, false) -- Regular layers
|
||||||
|
testNewLayerOrdering(true, false) -- Groups
|
||||||
|
testNewLayerOrdering(false, true) -- Tilemaps
|
||||||
|
end
|
||||||
|
|
||||||
do -- Background/Transparent layers
|
do -- Background/Transparent layers
|
||||||
local s = Sprite(32, 32)
|
local s = Sprite(32, 32)
|
||||||
assert(s.layers[1].isTransparent)
|
assert(s.layers[1].isTransparent)
|
||||||
|
|
|
@ -14,7 +14,7 @@ do
|
||||||
assert(b.bounds == Rectangle(0, 2, 8, 10))
|
assert(b.bounds == Rectangle(0, 2, 8, 10))
|
||||||
assert(c.bounds == Rectangle(0, 0, 32, 32))
|
assert(c.bounds == Rectangle(0, 0, 32, 32))
|
||||||
|
|
||||||
local bounds = { nil, Rectangle(0, 2, 8, 10), Rectangle(0, 0, 32, 32) }
|
local bounds = { Rectangle(0, 0, 32, 32), Rectangle(0, 2, 8, 10), nil }
|
||||||
|
|
||||||
local i = 1
|
local i = 1
|
||||||
for k,v in ipairs(s.slices) do
|
for k,v in ipairs(s.slices) do
|
||||||
|
@ -25,8 +25,8 @@ do
|
||||||
end
|
end
|
||||||
|
|
||||||
s:deleteSlice(b)
|
s:deleteSlice(b)
|
||||||
assert(a == s.slices[1])
|
assert(c == s.slices[1])
|
||||||
assert(c == s.slices[2])
|
assert(a == s.slices[2])
|
||||||
|
|
||||||
assert(2 == #s.slices)
|
assert(2 == #s.slices)
|
||||||
app.undo()
|
app.undo()
|
||||||
|
|
|
@ -228,3 +228,69 @@ do
|
||||||
c = app.open(fn)
|
c = app.open(fn)
|
||||||
assert(c.tileManagementPlugin == nil)
|
assert(c.tileManagementPlugin == nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Undo History
|
||||||
|
|
||||||
|
function test_undo_history()
|
||||||
|
local sprite = Sprite(1, 1)
|
||||||
|
|
||||||
|
assert(sprite.undoHistory.undoSteps == 0)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 0)
|
||||||
|
|
||||||
|
sprite:resize(10, 10)
|
||||||
|
|
||||||
|
assert(sprite.undoHistory.undoSteps == 1)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 0)
|
||||||
|
|
||||||
|
sprite:resize(10, 15)
|
||||||
|
|
||||||
|
assert(sprite.undoHistory.undoSteps == 2)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 0)
|
||||||
|
|
||||||
|
sprite:resize(10, 30)
|
||||||
|
|
||||||
|
assert(sprite.undoHistory.undoSteps == 3)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 0)
|
||||||
|
|
||||||
|
app.undo()
|
||||||
|
assert(sprite.undoHistory.undoSteps == 2)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 1)
|
||||||
|
|
||||||
|
app.undo()
|
||||||
|
assert(sprite.undoHistory.undoSteps == 1)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 2)
|
||||||
|
|
||||||
|
app.redo()
|
||||||
|
assert(sprite.undoHistory.undoSteps == 2)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 1)
|
||||||
|
|
||||||
|
app.undo()
|
||||||
|
app.undo()
|
||||||
|
|
||||||
|
assert(sprite.undoHistory.undoSteps == 0)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 3)
|
||||||
|
|
||||||
|
sprite:resize(10, 30)
|
||||||
|
|
||||||
|
if (app.preferences.undo.allow_nonlinear_history) then
|
||||||
|
assert(sprite.undoHistory.undoSteps == 4)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 0)
|
||||||
|
else
|
||||||
|
assert(sprite.undoHistory.undoSteps == 1)
|
||||||
|
assert(sprite.undoHistory.redoSteps == 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local prevSetting = app.preferences.undo.allow_nonlinear_history
|
||||||
|
app.preferences.undo.allow_nonlinear_history = true
|
||||||
|
test_undo_history()
|
||||||
|
app.preferences.undo.allow_nonlinear_history = prevSetting
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local prevSetting = app.preferences.undo.allow_nonlinear_history
|
||||||
|
app.preferences.undo.allow_nonlinear_history = false
|
||||||
|
test_undo_history()
|
||||||
|
app.preferences.undo.allow_nonlinear_history = prevSetting
|
||||||
|
end
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8607be393dfd81614611897a6e3026fe94a3966c
|
Subproject commit d14c2d1764f800d31b51893fb3d1e05d77a9280b
|
Loading…
Reference in New Issue