diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 673a802b7..000000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..20baa7603
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..9acbb5ce1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -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.
diff --git a/data/extensions/aseprite-theme/dark/sheet.png b/data/extensions/aseprite-theme/dark/sheet.png
index 5496d75ed..02bf218ac 100644
Binary files a/data/extensions/aseprite-theme/dark/sheet.png and b/data/extensions/aseprite-theme/dark/sheet.png differ
diff --git a/data/extensions/aseprite-theme/dark/theme.xml b/data/extensions/aseprite-theme/dark/theme.xml
index ba0c797a6..3c33fb4ac 100644
--- a/data/extensions/aseprite-theme/dark/theme.xml
+++ b/data/extensions/aseprite-theme/dark/theme.xml
@@ -1,1162 +1,1164 @@
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
diff --git a/data/extensions/aseprite-theme/sheet.png b/data/extensions/aseprite-theme/sheet.png
index ac3a332f3..cf1bc6157 100644
Binary files a/data/extensions/aseprite-theme/sheet.png and b/data/extensions/aseprite-theme/sheet.png differ
diff --git a/data/extensions/aseprite-theme/theme.xml b/data/extensions/aseprite-theme/theme.xml
index ba48be8b6..c7db24cc0 100644
--- a/data/extensions/aseprite-theme/theme.xml
+++ b/data/extensions/aseprite-theme/theme.xml
@@ -1,1152 +1,1154 @@
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
diff --git a/data/gui.xml b/data/gui.xml
index 2c0ca8e32..4a357e72b 100644
--- a/data/gui.xml
+++ b/data/gui.xml
@@ -1,6 +1,6 @@
-
+
@@ -372,6 +372,12 @@
+
+
+
+
+
+
@@ -1205,6 +1211,7 @@
diff --git a/data/strings/en.ini b/data/strings/en.ini
index 6fde1f9a0..4d4517fbd 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -206,6 +206,7 @@ AddColor_Background = Background
AddColor_Foreground = Foreground
AddColor_Specific = Specific
AdvancedMode = Advanced Mode
+Apply = Apply
AutocropSprite = Trim Sprite
AutocropSprite_ByGrid = Trim Sprite by Grid
BackgroundFromLayer = Background from Layer
@@ -265,6 +266,7 @@ Despeckle = Despeckle
DeveloperConsole = Developer Console
DiscardBrush = Discard Brush
DuplicateLayer = Duplicate Layer
+DuplicateSlice = Duplicate Slice
DuplicateSprite = Duplicate Sprite
DuplicateView = Duplicate View
Exit = Exit
@@ -582,8 +584,9 @@ rotsprite = RotSprite
pixel_perfect = Pixel-perfect
linear_gradient = Linear Gradient
radial_gradient = Radial Gradient
-drop_pixel = Drop pixels here (Enter)
-cancel_drag = Cancel drag and drop (Esc)
+drop_pixel_and_deselect = Apply transformation and deselect
+drop_pixel = Apply transformation and keep selection
+cancel_drag = Cancel transformation and undo/discard changes
auto_select_layer = Auto Select Layer
all = All
none = None
@@ -798,7 +801,22 @@ empty_fonts = No system fonts were found
[font_style]
antialias = Antialias
hinting = Hinting
+hinting_none = No Hinting
+hinting_slight = Slight Hinting
+hinting_full = Full Hinting
ligatures = Ligatures
+font_weight = Font Weight
+italic = Italic
+font_weight_100 = Thin
+font_weight_200 = Extra Light
+font_weight_300 = Light
+font_weight_400 = Normal
+font_weight_500 = Medium
+font_weight_600 = Semi Bold
+font_weight_700 = Bold
+font_weight_800 = Extra Bold
+font_weight_900 = Black
+font_weight_1000 = Extra Black
[frame_combo]
all_frames = All frames
@@ -827,6 +845,7 @@ opacity = Opacity:
tolerance = Tolerance:
show_more = Show more...
search = Search
+copy_of = {} Copy
[general_text]
copy = &Copy
@@ -967,6 +986,7 @@ key_context_move_tool = Move Tool
key_context_freehand_tool = Freehand Tool
key_context_shape_tool = Shape Tool
key_context_frames_selection = Frames Selection
+key_context_transformation = Transformation
copy_selection = Copy Selection
snap_to_grid = Snap To Grid
lock_axis = Lock Axis
@@ -1698,6 +1718,10 @@ from = From:
to = To:
tolerance = Tolerance:
+[duplicate_slice]
+x_duplicated = Slice "{}" duplicated
+n_slices_duplicated = {} slice(s) duplicated
+
[remove_slice]
x_removed = Slice "{}" removed
n_slices_removed = {} slice(s) removed
@@ -1782,6 +1806,7 @@ delete_file = Delete file, I've already sent it
[slice_popup_menu]
properties = Slice &Properties...
+duplicate = D&uplicate Slice
delete = &Delete Slice
[slice_properties]
diff --git a/data/widgets/slice_properties.xml b/data/widgets/slice_properties.xml
index a683919c5..5c6e70ad6 100644
--- a/data/widgets/slice_properties.xml
+++ b/data/widgets/slice_properties.xml
@@ -1,17 +1,17 @@
-
+
-
+
-
-
+
+
@@ -20,6 +20,7 @@
+
@@ -28,6 +29,7 @@
+
@@ -36,16 +38,18 @@
+
+
-
-
-
+
+
+
diff --git a/data/widgets/tag_properties.xml b/data/widgets/tag_properties.xml
index 4d0f2303c..268cb559d 100644
--- a/data/widgets/tag_properties.xml
+++ b/data/widgets/tag_properties.xml
@@ -1,6 +1,6 @@
-
+
@@ -23,13 +23,14 @@
+
-
+
-
+
-
+
diff --git a/laf b/laf
index 01571537b..3f1f86cc7 160000
--- a/laf
+++ b/laf
@@ -1 +1 @@
-Subproject commit 01571537bc6002a2e039a66497837365c394d7fa
+Subproject commit 3f1f86cc734443ba5c72d25c72cdf41ca208e9fe
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 4abc15e0a..b85fcaf5e 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -74,7 +74,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${output_fn}.tmp ${output_fn}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
MAIN_DEPENDENCY ${strings_en_ini}
- DEPENDS ${GEN_DEP})
+ DEPENDS ${GEN_DEP} commands/commands_list.h)
list(APPEND generated_files ${output_fn})
# Check translations
@@ -368,6 +368,7 @@ target_sources(app-lib PRIVATE
color_picker.cpp
color_spaces.cpp
color_utils.cpp
+ commands/apply.cpp
commands/cmd_about.cpp
commands/cmd_add_color.cpp
commands/cmd_advanced_mode.cpp
@@ -393,6 +394,7 @@ target_sources(app-lib PRIVATE
commands/cmd_deselect_mask.cpp
commands/cmd_discard_brush.cpp
commands/cmd_duplicate_layer.cpp
+ commands/cmd_duplicate_slice.cpp
commands/cmd_duplicate_sprite.cpp
commands/cmd_duplicate_view.cpp
commands/cmd_enter_license.cpp
@@ -653,6 +655,7 @@ target_sources(app-lib PRIVATE
ui/icon_button.cpp
ui/incompat_file_window.cpp
ui/input_chain.cpp
+ ui/key.cpp
ui/keyboard_shortcuts.cpp
ui/layer_frame_comboboxes.cpp
ui/layout.cpp
@@ -720,6 +723,7 @@ target_sources(app-lib PRIVATE
util/render_text.cpp
util/resize_image.cpp
util/shader_helpers.cpp
+ util/slice_utils.cpp
util/tile_flags_utils.cpp
util/tileset_utils.cpp
util/wrap_point.cpp
diff --git a/src/app/commands/apply.cpp b/src/app/commands/apply.cpp
new file mode 100644
index 000000000..693ad8c89
--- /dev/null
+++ b/src/app/commands/apply.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())
+{
+}
+
+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
diff --git a/src/app/commands/cmd_cel_properties.cpp b/src/app/commands/cmd_cel_properties.cpp
index fd7b4f85d..d5a7bb0e0 100644
--- a/src/app/commands/cmd_cel_properties.cpp
+++ b/src/app/commands/cmd_cel_properties.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2020-2023 Igara Studio S.A.
+// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -100,12 +100,12 @@ public:
if (countCels() > 0) {
m_userDataView.configureAndSet((m_cel ? m_cel->data()->userData() : UserData()),
- g_window->propertiesGrid());
+ propertiesGrid());
}
else if (!m_cel)
m_userDataView.setVisible(false, false);
- g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
+ expandWindow(gfx::Size(bounds().w, sizeHint().h));
updateFromCel();
}
@@ -280,7 +280,7 @@ private:
{
if (countCels() > 0) {
m_userDataView.toggleVisibility();
- g_window->expandWindow(gfx::Size(g_window->bounds().w, g_window->sizeHint().h));
+ expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
}
diff --git a/src/app/commands/cmd_duplicate_slice.cpp b/src/app/commands/cmd_duplicate_slice.cpp
new file mode 100644
index 000000000..5f745e3f8
--- /dev/null
+++ b/src/app/commands/cmd_duplicate_slice.cpp
@@ -0,0 +1,121 @@
+// 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())
+{
+}
+
+void DuplicateSliceCommand::onLoadParams(const Params& params)
+{
+ std::string id = params.get("id");
+ if (!id.empty())
+ m_sliceId = ObjectId(base::convert_to(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 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(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
diff --git a/src/app/commands/cmd_keyboard_shortcuts.cpp b/src/app/commands/cmd_keyboard_shortcuts.cpp
index c85fcc9d7..bcdd3d68e 100644
--- a/src/app/commands/cmd_keyboard_shortcuts.cpp
+++ b/src/app/commands/cmd_keyboard_shortcuts.cpp
@@ -264,7 +264,7 @@ private:
if (m_key && m_key->keycontext() != KeyContext::Any) {
int w = m_headerItem->contextXPos() +
- font()->textLength(convertKeyContextToUserFriendlyString(m_key->keycontext()));
+ font()->textLength(convert_keycontext_to_user_friendly_string(m_key->keycontext()));
size.w = std::max(size.w, w);
}
@@ -312,7 +312,7 @@ private:
if (m_key && !m_key->shortcuts().empty()) {
if (m_key->keycontext() != KeyContext::Any) {
- g->drawText(convertKeyContextToUserFriendlyString(m_key->keycontext()),
+ g->drawText(convert_keycontext_to_user_friendly_string(m_key->keycontext()),
fg,
bg,
gfx::Point(contextXPos, y));
@@ -590,7 +590,7 @@ private:
case KeyContext::MoveTool:
case KeyContext::FreehandTool:
case KeyContext::ShapeTool:
- text = convertKeyContextToUserFriendlyString(key->keycontext()) + ": " + text;
+ text = convert_keycontext_to_user_friendly_string(key->keycontext()) + ": " + text;
break;
}
KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key, nullptr, 0, &m_headerItem);
diff --git a/src/app/commands/cmd_layer_properties.cpp b/src/app/commands/cmd_layer_properties.cpp
index 869a9b897..e3e210eb7 100644
--- a/src/app/commands/cmd_layer_properties.cpp
+++ b/src/app/commands/cmd_layer_properties.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2020-2024 Igara Studio S.A.
+// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -165,7 +165,7 @@ public:
m_document->add_observer(this);
if (countLayers() > 0) {
- m_userDataView.configureAndSet(m_layer->userData(), g_window->propertiesGrid());
+ m_userDataView.configureAndSet(m_layer->userData(), propertiesGrid());
if (m_remapAfterConfigure) {
remapWindow();
centerWindow();
@@ -367,8 +367,7 @@ private:
{
if (m_layer) {
m_userDataView.toggleVisibility();
- g_window->remapWindow();
- manager()->invalidate();
+ expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
}
diff --git a/src/app/commands/cmd_new_layer.cpp b/src/app/commands/cmd_new_layer.cpp
index 2a610996f..0c45386e4 100644
--- a/src/app/commands/cmd_new_layer.cpp
+++ b/src/app/commands/cmd_new_layer.cpp
@@ -264,16 +264,8 @@ void NewLayerCommand::onExecute(Context* context)
bool afterBackground = false;
switch (m_type) {
- case Type::Layer:
-
- if (m_place == Place::BeforeActiveLayer) {
- layer = api.newLayer(parent, name);
- api.restackLayerBefore(layer, parent, activeLayer);
- }
- else
- layer = api.newLayerAfter(parent, name, activeLayer);
- break;
- case Type::Group: layer = api.newGroupAfter(parent, name, activeLayer); break;
+ case Type::Layer: layer = api.newLayer(parent, name); break;
+ case Type::Group: layer = api.newGroup(parent, name); break;
case Type::ReferenceLayer:
layer = api.newLayer(parent, name);
if (layer)
@@ -308,6 +300,15 @@ void NewLayerCommand::onExecute(Context* context)
ASSERT(layer->parent());
+ // Reorder the resulting layer.
+ switch (m_place) {
+ case Place::AfterActiveLayer: api.restackLayerAfter(layer, parent, activeLayer); break;
+ case Place::BeforeActiveLayer: api.restackLayerBefore(layer, parent, activeLayer); break;
+ case Place::Top:
+ api.restackLayerAfter(layer, sprite->root(), sprite->root()->lastLayer());
+ break;
+ }
+
// Put new layer as an overlay of the background or in the first
// layer in case the sprite is transparent.
if (afterBackground) {
diff --git a/src/app/commands/cmd_sprite_properties.cpp b/src/app/commands/cmd_sprite_properties.cpp
index 900acfbfb..d5d7d0a96 100644
--- a/src/app/commands/cmd_sprite_properties.cpp
+++ b/src/app/commands/cmd_sprite_properties.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2018-2024 Igara Studio S.A.
+// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -187,8 +187,7 @@ private:
void onToggleUserData()
{
m_userDataView.toggleVisibility();
- remapWindow();
- manager()->invalidate();
+ expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
void onTilesedDuplicated(const Tileset* tilesetClone)
diff --git a/src/app/commands/commands_list.h b/src/app/commands/commands_list.h
index 5a32f7495..210cb59fb 100644
--- a/src/app/commands/commands_list.h
+++ b/src/app/commands/commands_list.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2018-2024 Igara Studio S.A.
+// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -8,6 +8,7 @@
FOR_EACH_COMMAND(About)
FOR_EACH_COMMAND(AddColor)
FOR_EACH_COMMAND(AdvancedMode)
+FOR_EACH_COMMAND(Apply)
FOR_EACH_COMMAND(AutocropSprite)
FOR_EACH_COMMAND(BackgroundFromLayer)
FOR_EACH_COMMAND(BrightnessContrast)
@@ -40,6 +41,7 @@ FOR_EACH_COMMAND(DeselectMask)
FOR_EACH_COMMAND(Despeckle)
FOR_EACH_COMMAND(DiscardBrush)
FOR_EACH_COMMAND(DuplicateLayer)
+FOR_EACH_COMMAND(DuplicateSlice)
FOR_EACH_COMMAND(DuplicateSprite)
FOR_EACH_COMMAND(DuplicateView)
FOR_EACH_COMMAND(Exit)
diff --git a/src/app/console.cpp b/src/app/console.cpp
index f1954d1c6..a29112658 100644
--- a/src/app/console.cpp
+++ b/src/app/console.cpp
@@ -81,7 +81,7 @@ public:
~ConsoleWindow() { TRACE_CON("CON: ~ConsoleWindow this=", this); }
- void addMessage(const std::string& msg)
+ void addMessage(std::string msg)
{
if (!m_hasText) {
m_hasText = true;
@@ -93,6 +93,17 @@ public:
gfx::Point pt = m_view.viewScroll();
const bool autoScroll = (pt.y >= maxSize.h - visible.h);
+ // Escape characters we can't show properly
+ for (size_t i = 0; i < msg.size(); i++) {
+ switch (msg[i]) {
+ case '\a':
+ case '\b':
+ case '\r':
+ case '\t':
+ case '\v': msg[i] = ' ';
+ }
+ }
+
m_textbox.setText(m_textbox.text() + msg);
if (autoScroll) {
diff --git a/src/app/doc.cpp b/src/app/doc.cpp
index 21a8708b1..2523aa04c 100644
--- a/src/app/doc.cpp
+++ b/src/app/doc.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2018-2024 Igara Studio S.A.
+// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -338,6 +338,19 @@ void Doc::notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti)
notify_observers(&DocObserver::onAfterAddTile, ev);
}
+void Doc::notifyBeforeSlicesDuplication()
+{
+ DocEvent ev(this);
+ notify_observers(&DocObserver::onBeforeSlicesDuplication, ev);
+}
+
+void Doc::notifySliceDuplicated(Slice* slice)
+{
+ DocEvent ev(this);
+ ev.slice(slice);
+ notify_observers(&DocObserver::onSliceDuplicated, ev);
+}
+
bool Doc::isModified() const
{
return !m_undo->isInSavedStateOrSimilar();
diff --git a/src/app/doc.h b/src/app/doc.h
index 66166b68e..f6dd07245 100644
--- a/src/app/doc.h
+++ b/src/app/doc.h
@@ -141,6 +141,8 @@ public:
void notifyTilesetChanged(Tileset* tileset);
void notifyLayerGroupCollapseChange(Layer* layer);
void notifyAfterAddTile(LayerTilemap* layer, frame_t frame, tile_index ti);
+ void notifyBeforeSlicesDuplication();
+ void notifySliceDuplicated(Slice* slice);
//////////////////////////////////////////////////////////////////////
// File related properties
diff --git a/src/app/doc_observer.h b/src/app/doc_observer.h
index 033c91516..208a5b0a7 100644
--- a/src/app/doc_observer.h
+++ b/src/app/doc_observer.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2018-2024 Igara Studio S.A.
+// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -90,6 +90,8 @@ public:
// Slices
virtual void onSliceNameChange(DocEvent& ev) {}
+ virtual void onBeforeSlicesDuplication(DocEvent& ev) {}
+ virtual void onSliceDuplicated(DocEvent& ev) {}
// The tileset has changed.
virtual void onTilesetChanged(DocEvent& ev) {}
diff --git a/src/app/fonts/font_info.cpp b/src/app/fonts/font_info.cpp
index 370f805c7..c32c4dac9 100644
--- a/src/app/fonts/font_info.cpp
+++ b/src/app/fonts/font_info.cpp
@@ -11,6 +11,7 @@
#include "app/fonts/font_info.h"
#include "app/fonts/font_data.h"
+#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "base/fs.h"
#include "base/split_string.h"
@@ -142,19 +143,22 @@ std::string FontInfo::humanString() const
}
result += fmt::format(" {}pt", size());
if (!result.empty()) {
- if (style().weight() >= text::FontStyle::Weight::SemiBold)
- result += " Bold";
+ if (style().weight() != text::FontStyle::Weight::Normal)
+ result +=
+ " " +
+ Strings::Translate(
+ fmt::format("font_style.font_weight_{}", static_cast(style().weight())).c_str());
if (style().slant() != text::FontStyle::Slant::Upright)
- result += " Italic";
+ result += " " + Strings::font_style_italic();
if (antialias())
- result += " Antialias";
+ result += " " + Strings::font_style_antialias();
if (ligatures())
- result += " Ligatures";
+ result += " " + Strings::font_style_ligatures();
switch (hinting()) {
- case text::FontHinting::None: result += " No Hinting"; break;
- case text::FontHinting::Slight: result += " Slight Hinting"; break;
+ case text::FontHinting::None: result += " " + Strings::font_style_hinting_none(); break;
+ case text::FontHinting::Slight: result += " " + Strings::font_style_hinting_slight(); break;
case text::FontHinting::Normal: break;
- case text::FontHinting::Full: result += " Full Hinting"; break;
+ case text::FontHinting::Full: result += " " + Strings::font_style_hinting_full(); break;
}
}
return result;
@@ -177,6 +181,7 @@ app::FontInfo convert_to(const std::string& from)
bool italic = false;
app::FontInfo::Flags flags = app::FontInfo::Flags::None;
text::FontHinting hinting = text::FontHinting::Normal;
+ text::FontStyle::Weight weight = text::FontStyle::Weight::Normal;
if (!parts.empty()) {
if (parts[0].compare(0, 5, "file=") == 0) {
@@ -214,16 +219,17 @@ app::FontInfo convert_to(const std::string& from)
else if (hintingStr == "full")
hinting = text::FontHinting::Full;
}
+ else if (parts[i].compare(0, 7, "weight=") == 0) {
+ std::string weightStr = parts[i].substr(7);
+ weight = static_cast(std::atoi(weightStr.c_str()));
+ }
}
}
- text::FontStyle style;
- if (bold && italic)
- style = text::FontStyle::BoldItalic();
- else if (bold)
- style = text::FontStyle::Bold();
- else if (italic)
- style = text::FontStyle::Italic();
+ const text::FontStyle style(
+ bold ? text::FontStyle::Weight::Bold : weight,
+ text::FontStyle::Width::Normal,
+ italic ? text::FontStyle::Slant::Italic : text::FontStyle::Slant::Upright);
return app::FontInfo(type, name, size, style, flags, hinting);
}
@@ -243,7 +249,7 @@ std::string convert_to(const app::FontInfo& from)
if (!result.empty()) {
if (from.size() > 0.0f)
result += fmt::format(",size={}", from.size());
- if (from.style().weight() >= text::FontStyle::Weight::SemiBold)
+ if (from.style().weight() == text::FontStyle::Weight::Bold)
result += ",bold";
if (from.style().slant() != text::FontStyle::Slant::Upright)
result += ",italic";
@@ -262,6 +268,8 @@ std::string convert_to(const app::FontInfo& from)
case text::FontHinting::Full: result += "full"; break;
}
}
+ if (from.style().weight() != text::FontStyle::Weight::Bold)
+ result += ",weight=" + std::to_string(static_cast(from.style().weight()));
}
return result;
}
diff --git a/src/app/modules/gui.cpp b/src/app/modules/gui.cpp
index f42d7c503..d0b034743 100644
--- a/src/app/modules/gui.cpp
+++ b/src/app/modules/gui.cpp
@@ -681,81 +681,90 @@ void CustomizedGuiManager::onNewDisplayConfiguration(Display* display)
bool CustomizedGuiManager::processKey(Message* msg)
{
App* app = App::instance();
+ const KeyContext currentCtx = KeyboardShortcuts::getCurrentKeyContext();
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);
+
+ // Find best match (prefer the shortcut that matches the context first)
+ KeyPtr key = nullptr;
for (int i = 0; i < n; ++i) {
- for (const KeyPtr& key : *keys) {
- if (key->isPressed(msg, contexts[i])) {
- // Cancel menu-bar loops (to close any popup menu)
- app->mainWindow()->getMenuBar()->cancelMenuLoop();
-
- 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 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;
+ for (const KeyPtr& k : *keys) {
+ if (k->isPressed(msg, contexts[i]) &&
+ (!key ||
+ (key->keycontext() != currentCtx && match_key_context(k->keycontext(), currentCtx)))) {
+ key = k;
}
}
}
+ if (!key)
+ return false;
+
+ // Cancel menu-bar loops (to close any popup menu)
+ 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 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;
}
diff --git a/src/app/script/app_object.cpp b/src/app/script/app_object.cpp
index e09bfed56..ac59ba4bc 100644
--- a/src/app/script/app_object.cpp
+++ b/src/app/script/app_object.cpp
@@ -34,10 +34,10 @@
#include "app/tools/tool_loop_manager.h"
#include "app/tx.h"
#include "app/ui/context_bar.h"
-#include "app/ui/doc_view.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/tool_loop_impl.h"
#include "app/ui/main_window.h"
+#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui_context.h"
#include "base/fs.h"
@@ -498,6 +498,44 @@ int App_useTool(lua_State* L)
return 0;
}
+int App_tip(lua_State* L)
+{
+ const auto* ctx = App::instance()->context();
+ if (!ctx || !ctx->isUIAvailable() || !StatusBar::instance())
+ return 0; // No UI to show the tooltip
+
+ std::string text;
+ double duration = 2.0;
+
+ if (lua_istable(L, 1)) {
+ int type = lua_getfield(L, 1, "text");
+ if (type == LUA_TSTRING)
+ text = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ type = lua_getfield(L, 1, "duration");
+ if (type == LUA_TNUMBER)
+ duration = lua_tonumber(L, -1);
+ lua_pop(L, 1);
+ }
+ else {
+ if (!lua_isstring(L, 1))
+ return luaL_error(L, "app.tip text parameter must be a string");
+
+ text = lua_tostring(L, 1);
+
+ if (lua_isnumber(L, 2))
+ duration = lua_tonumber(L, 2);
+ }
+
+ if (text.empty())
+ return luaL_error(L, "app.tip text cannot be empty");
+
+ int msecs = std::clamp(duration * 1000.0, 500, 30000);
+ StatusBar::instance()->showTip(msecs, text);
+ return 0;
+}
+
int App_get_events(lua_State* L)
{
push_app_events(L);
@@ -820,6 +858,7 @@ const luaL_Reg App_methods[] = {
{ "alert", App_alert },
{ "refresh", App_refresh },
{ "useTool", App_useTool },
+ { "tip", App_tip },
{ nullptr, nullptr }
};
diff --git a/src/app/script/dialog_class.cpp b/src/app/script/dialog_class.cpp
index 7fa02674e..c42e7153b 100644
--- a/src/app/script/dialog_class.cpp
+++ b/src/app/script/dialog_class.cpp
@@ -60,6 +60,8 @@ namespace app { namespace script {
using namespace ui;
+static constexpr const int kDefaultAutofit = ui::LEFT | ui::TOP;
+
namespace {
class DialogWindow : public WindowWithHand {
@@ -107,6 +109,7 @@ struct Dialog {
std::map dataWidgets;
std::map labelWidgets;
int currentRadioGroup = 0;
+ int autofit = kDefaultAutofit;
// Member used to hold current state about the creation of a tabs
// widget. After creation it is reset to null to be ready for the
@@ -193,12 +196,20 @@ struct Dialog {
it->second->setText(text);
}
+ void setAutofit(int align)
+ {
+ // Accept both 0 or a valid subset of align parameters.
+ if (align == 0 || (align & (ui::LEFT | ui::RIGHT | ui::TOP | ui::BOTTOM)))
+ autofit = align;
+ }
+
Display* parentDisplay() const
{
Display* parentDisplay = window.parentDisplay();
if (!parentDisplay) {
- const auto mainWindow = App::instance()->mainWindow();
- parentDisplay = mainWindow->display();
+ const auto* mainWindow = App::instance()->mainWindow();
+ if (mainWindow)
+ parentDisplay = mainWindow->display();
}
return parentDisplay;
}
@@ -210,6 +221,9 @@ struct Dialog {
// origin/scale (or main window if a parent window wasn't specified).
if (window.ownDisplay()) {
const Display* parentDisplay = this->parentDisplay();
+ if (!parentDisplay)
+ return bounds;
+
const int scale = parentDisplay->scale();
const gfx::Point dialogOrigin = window.display()->nativeWindow()->contentRect().origin();
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
@@ -224,6 +238,9 @@ struct Dialog {
window.expandWindow(rc.size());
Display* parentDisplay = this->parentDisplay();
+ if (!parentDisplay)
+ return;
+
const int scale = parentDisplay->scale();
const gfx::Point mainOrigin = parentDisplay->nativeWindow()->contentRect().origin();
gfx::Rect frame = window.display()->nativeWindow()->contentRect();
@@ -231,8 +248,10 @@ struct Dialog {
window.display()->nativeWindow()->setFrame(frame);
}
else {
+ gfx::Rect oldBounds(window.bounds());
window.setBounds(rc);
window.invalidate();
+ parentDisplay()->invalidateRect(oldBounds);
}
}
@@ -367,6 +386,7 @@ int Dialog_new(lua_State* L)
ui::Window::Type windowType = ui::Window::WithTitleBar;
std::string title = "Script";
bool sizeable = true;
+ int autofit = kDefaultAutofit;
if (lua_isstring(L, 1)) {
title = lua_tostring(L, 1);
}
@@ -385,9 +405,16 @@ int Dialog_new(lua_State* L)
if (type != LUA_TNIL && !lua_toboolean(L, -1))
sizeable = false;
lua_pop(L, 1);
+
+ type = lua_getfield(L, 1, "autofit");
+ if (type != LUA_TNIL) {
+ autofit = lua_tointeger(L, -1);
+ }
+ lua_pop(L, 1);
}
auto dlg = push_new