Merge branch 'main' into beta

This commit is contained in:
David Capello 2024-09-23 14:21:01 -03:00
commit e700cce987
133 changed files with 2832 additions and 874 deletions

View File

@ -256,16 +256,15 @@ elseif(NOT LAF_BACKEND STREQUAL "skia")
set(FREETYPE_LIBRARIES ${FREETYPE_LIBRARY})
set(FREETYPE_INCLUDE_DIRS ${FREETYPE_DIR}/include)
endif()
include_directories(${FREETYPE_INCLUDE_DIRS})
# harfbuzz
if(USE_SHARED_HARFBUZZ)
find_package(HarfBuzz)
elseif(NOT LAF_BACKEND STREQUAL "skia")
set(HARFBUZZ_FOUND ON)
set(HARFBUZZ_LIBRARIES harfbuzz)
set(HARFBUZZ_INCLUDE_DIRS ${HARFBUZZ_DIR}/src)
endif()
include_directories(${HARFBUZZ_INCLUDE_DIRS})
if(USE_SHARED_GIFLIB)
find_package(GIF REQUIRED)
@ -327,11 +326,16 @@ if(ENABLE_WEBP)
NAMES libwebp # required for Windows
PATHS "${SKIA_LIBRARY_DIR}" NO_DEFAULT_PATH)
set(WEBP_INCLUDE_DIR "${SKIA_DIR}/third_party/externals/libwebp/src")
if(WEBP_LIBRARIES)
set(WEBP_FOUND ON)
else()
set(WEBP_FOUND OFF)
endif()
else()
set(WEBP_FOUND ON)
set(WEBP_LIBRARIES webp webpdemux libwebpmux)
set(WEBP_INCLUDE_DIR ${LIBWEBP_DIR}/src)
endif()
include_directories(${WEBP_INCLUDE_DIR})
endif()
# Print paths to used libraries

View File

@ -106,6 +106,28 @@ We have some rules for the changes and commits that are contributed:
You can also take a look at the [src/README.md](https://github.com/aseprite/aseprite/tree/main/src/#aseprite-source-code)
guide which contains some information about how the code is structured.
## Pull Request (PR) Assignee
In case you are a developer or contributor with write or triage access
to the repository:
1. The PR assignee is the one that is working on the PR right now.
2. After a PR is sent, you (can) assign the PR to some other developer
that will act as a reviewer.
* Or if there is no assignee and the PR is not a draft, some
developer will take it for review sooner or later.
3. That developer will review the PR (or reassign the PR).
4. When the review process is done, the reviewer will merge the PR or
reassign the PR to you if it needs some changes.
5. When you have applied the requested changes, you can reassign the
PR to the last reviewer.
6. If a PR is labeled with some "needs *something*" label, it means
that the PR will not merged as it is, and *something* is required
to continue.
With this workflow you can find the PRs assigned to you to
review/continue working in: https://github.com/pulls/assigned
# Community
You can use the [Development category](https://community.aseprite.org/c/development)

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2018-2023 Igara Studio S.A. -->
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
<!-- Copyright (C) 2001-2018 David Capello -->
<gui>
<!-- Keyboard shortcuts -->
@ -289,11 +289,9 @@
<key command="Zoom" shortcut="3"><param name="percentage" value="400" /></key>
<key command="Zoom"><param name="percentage" value="500" /></key>
<key command="Zoom"><param name="percentage" value="600" /></key>
<key command="Zoom"><param name="percentage" value="700" /></key>
<key command="Zoom" shortcut="4"><param name="percentage" value="800" /></key>
<key command="Zoom"><param name="percentage" value="1000" /></key>
<key command="Zoom" shortcut="5"><param name="percentage" value="1600" /></key>
<key command="Zoom"><param name="percentage" value="2000" /></key>
<key command="Zoom"><param name="percentage" value="2400" /></key>
<key command="Zoom" shortcut="6"><param name="percentage" value="3200" /></key>
<key command="Zoom" shortcut="Ctrl++" mac="Cmd++">
<param name="action" value="in" />
@ -479,9 +477,6 @@
<key command="NewSpriteFromSelection" shortcut="Ctrl+Alt+N" mac="Cmd+Alt+N" />
<!-- Commands not associated to menu items and without shortcuts by default -->
<key command="ExportSpriteSheet">
<param name="source" value="tileset" />
</key>
<key command="NewLayer">
<param name="tilemap" value="true" />
</key>
@ -539,7 +534,7 @@
</key>
<key command="ChangePixelFormat">
<param name="format" value="indexed" />
<param name="dithering" value="old-ordered" />
<param name="dithering" value="old" />
</key>
<key command="ChangeBrush">
<param name="change" value="increment-angle" />
@ -1102,7 +1097,7 @@
</item>
<item command="NewFrame" text="@main_menu.frame_new_frame" />
</menu>
<menu id="tab_popup_menu">
<item command="CloseFile" text="@.close" group="tab_close" />
</menu>

View File

@ -322,6 +322,7 @@
<option id="to_gray" type="ToGrayAlgorithm" default="ToGrayAlgorithm::DEFAULT" />
<option id="advanced" type="bool" default="false" />
<option id="rgbmap_algorithm" type="doc::RgbMapAlgorithm" default="doc::RgbMapAlgorithm::DEFAULT" />
<option id="fit_criteria" type="doc::FitCriteria" default="doc::FitCriteria::DEFAULT" />
</section>
<section id="eyedropper" text="Editor">
<option id="channel" type="EyedropperChannel" default="EyedropperChannel::COLOR_ALPHA" />

View File

@ -111,6 +111,7 @@ overwrite_existent_file = Warning<<File exists, overwrite it?<<{0}||&Yes||&No||&
overwrite_files_on_export_sprite_sheet = Export Sprite Sheet Warning\n<<Do you want to overwrite the following file(s)?\n{0}\n||&Yes||&No
overwrite_files_on_export = Export Warning\n<<Do you want to overwrite the following file?\n<<{0}\n||&Yes||&No
enter_license_disabled = Information\n<<This copy of Aseprite does not support entering a license key.\n<<Consider getting one from https://aseprite.org/download.\n<<Activating Aseprite will give you access to automatic updates.\n||&OK
reset_default_confirm = Resetting Preferences\n<<Are you sure you want to reset the selected preferences to their default values?\n||&Yes||&No
[brightness_contrast]
title = Brightness/Contrast
@ -280,6 +281,8 @@ Flip_Canvas = Canvas
Flip_Horizontally = Horizontally
Flip_Selection = Selection
Flip_Vertically = Vertically
FrameProperties_All = Frame Properties of all frames
FrameProperties_Current = Frame Properties of the current range
FrameProperties = Frame Properties
FrameTagProperties = Tag Properties
FullscreenMode = Toggle Fullscreen Mode
@ -313,6 +316,7 @@ LayerVisibility = Layer Visibility
LinkCels = Links Cels
LoadMask = Load Selection
LoadPalette = Load Palette
LoadDefaultPalette = Load Default Palette
MaskAll = Mask All
MaskByColor = Mask By Color
MaskContent = Mask Content
@ -404,6 +408,8 @@ SaveFileAs = Save File As
SaveFileCopyAs = Export
SaveMask = Save Selection
SavePalette = Save Palette
SavePaletteAsDefault = Save Palette as Default
SavePaletteAsPreset = Save Palette as Preset
Screenshot = Screenshot
Screenshot_Open = Take & Open Screenshot
Screenshot_Save = Take & Save Screenshot
@ -458,7 +464,11 @@ SwitchColors = Switch Colors
SwapCheckerboardColors = Swap Checkerboard Background Colors
SwitchNonactiveLayersOpacity = Switch Nonactive Layers Opacity
SymmetryMode = Symmetry Mode
TiledMode = Tiled Mode
TiledMode = Tiled Mode: {}
TiledMode_None = None
TiledMode_Both = Both Axes
TiledMode_X = X Axis
TiledMode_Y = Y Axis
Timeline = Switch Timeline
ToggleOtherLayersOpacity = Toggle Other Layers Opacity
ToggleOtherLayersOpacity_PreviewEditor = Toggle Other Layers Opacity in Preview
@ -1259,6 +1269,14 @@ default = Default (Octree)
rgb5a3 = Table RGB 5 bits + Alpha 3 bits
octree = Octree
[best_fit_criteria_selector]
label = Color Best Fit Criteria:
default = Default (Euclidean)
rgb = RGB
linearized_rgb = Linearized RGB
cie_xyz = CIEXYZ
cie_lab = CIELAB
[open_file]
title = Open
loading = Loading file
@ -1518,6 +1536,14 @@ set_cursor_fix_tooltip = Sets the mouse position to the pen location when\nyou h
wintab_more_info = (More Information)
flash_selected_layer = Flash layer when it is selected
non_active_layer_opacity = Opacity for non-active layers:
reset_title = Reset Preferences
reset_default = Reset configuration options available in the Preferences window
reset_tools = Reset all tool preferences
reset_installed = Remove installed themes, extensions, and palettes
reset_recents = Clear the recently opened file list (including pinned files)
reset_perfile = Remove any per-file settings
reset_perfile_tooltip = These are specific to opened files and includes\nthings like grid options, background colors, etc.
reset = &Reset
ok = &OK
apply = &Apply
cancel = &Cancel
@ -1856,3 +1882,4 @@ toggle_horizontal = Toggle Horizontal Symmetry
toggle_vertical = Toggle Vertical Symmetry
show_options = Symmetry Options
reset_position = Reset Symmetry to Center
reset_position_to_view_center = Reset Symmetry to View Center

View File

@ -1,5 +1,5 @@
<!-- Aseprite -->
<!-- Copyright (C) 2019-2020 Igara Studio S.A. -->
<!-- Copyright (C) 2019-2024 Igara Studio S.A. -->
<!-- Copyright (C) 2017 David Capello -->
<gui>
<window id="color_mode" text="@.title">
@ -22,11 +22,14 @@
<check text="@.flatten" id="flatten" />
<check id="advanced_check" text="@general.advanced_options" cell_hspan="2" />
<hbox id="advanced" cell_hspan="2">
<check id="advanced_check" text="@general.advanced_options" />
<grid id="advanced" columns="2">
<label text="@rgbmap_algorithm_selector.label" />
<hbox id="rgbmap_algorithm_placeholder" />
</hbox>
<hbox id="rgbmap_algorithm_placeholder" cell_align="horizontal" />
<label text="@best_fit_criteria_selector.label" />
<hbox id="best_fit_criteria_placeholder" cell_align="horizontal" />
</grid>
<separator horizontal="true" />
<hbox>

View File

@ -23,6 +23,8 @@
<listitem text="@.section_theme" value="section_theme" />
<listitem text="@.section_extensions" value="section_extensions" />
<listitem text="@.section_experimental" value="section_experimental" />
<separator horizontal="true" style="separator_in_view" />
<listitem text="@general.reset" value="section_reset" />
</listbox>
</view>
@ -515,14 +517,16 @@
pref="color_bar.show_invalid_fg_bg_color_alert" />
<check id="run_script_alert" text="@.run_script_alert"
pref="scripts.show_run_script_alert" />
<hbox>
<grid columns="4">
<label text="@.image_format_alerts" />
<check id="css_options_alert" text="!css" pref="css.show_alert" />
<check id="gif_options_alert" text="!gif" pref="gif.show_alert" />
<check id="jpeg_options_alert" text="!jpeg" pref="jpeg.show_alert" />
<boxfiller />
<check id="svg_options_alert" text="!svg" pref="svg.show_alert" />
<check id="tga_options_alert" text="!tga" pref="tga.show_alert" />
</hbox>
<check id="webp_options_alert" text="!webp" pref="webp.show_alert" />
</grid>
<separator horizontal="true" />
<hbox>
<hbox expansive="true" />
@ -592,10 +596,12 @@
<slider id="nonactive_layers_opacity" min="0" max="255" width="128" />
</hbox>
<separator text="@.color_quantization" horizontal="true" />
<hbox>
<grid columns="2">
<label text="@rgbmap_algorithm_selector.label" />
<hbox id="rgbmap_algorithm_placeholder" />
</hbox>
<label text="@best_fit_criteria_selector.label" />
<hbox id="best_fit_criteria_placeholder" />
</grid>
<separator text="@.performance" horizontal="true" />
<hbox>
<check id="shaders_for_color_selectors"
@ -608,6 +614,19 @@
pref="tileset.cache_compressed_tilesets" />
</vbox>
<!-- Reset -->
<vbox id="section_reset">
<separator text="@.reset_title" horizontal="true" />
<check id="default_reset" text="@.reset_default" />
<check id="tools_reset" text="@.reset_tools" />
<check id="installed_reset" text="@.reset_installed" />
<check id="recent_reset" text="@.reset_recents" />
<check id="perfile_reset" text="@.reset_perfile" tooltip="@.reset_perfile_tooltip" />
<hbox>
<hbox expansive="true" />
<button id="reset_selected_button" text="@.reset" minwidth="60" />
</hbox>
</vbox>
</panel>
</hbox>
<separator horizontal="true" />

View File

@ -48,6 +48,7 @@
</vbox>
<separator horizontal="true" />
<hbox>
<check text="@general.dont_show" id="dont_show" tooltip="@general.dont_show_tooltip" />
<boxfiller />
<hbox homogeneous="true">
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />

2
laf

@ -1 +1 @@
Subproject commit af450af5f6fabc24a267ee56d6cea2fb1e69b323
Subproject commit 851bdbc2454aa381131cd0972781aa86e149504a

View File

@ -143,7 +143,7 @@ target_sources(app-lib PRIVATE
file/qoi_format.cpp
file/svg_format.cpp
file/tga_format.cpp)
if(ENABLE_WEBP)
if(ENABLE_WEBP AND WEBP_FOUND)
target_compile_definitions(app-lib PUBLIC -DENABLE_WEBP)
target_sources(app-lib PRIVATE
file/webp_format.cpp)
@ -220,6 +220,7 @@ if(ENABLE_SCRIPTING)
script/range_class.cpp
script/rectangle_class.cpp
script/require.cpp
script/script_input_chain.cpp
script/security.cpp
script/selection_class.cpp
script/site_class.cpp
@ -581,6 +582,7 @@ target_sources(app-lib PRIVATE
ui/alpha_slider.cpp
ui/app_menuitem.cpp
ui/backup_indicator.cpp
ui/best_fit_criteria_selector.cpp
ui/browser_view.cpp
ui/brush_popup.cpp
ui/button_set.cpp
@ -751,10 +753,7 @@ target_link_libraries(app-lib
${TINYXML_LIBRARY}
${GIF_LIBRARIES}
${PNG_LIBRARIES}
${WEBP_LIBRARIES}
${ZLIB_LIBRARIES}
${FREETYPE_LIBRARIES}
${HARFBUZZ_LIBRARIES}
libjpeg-turbo
json11
archive_static
@ -762,6 +761,21 @@ target_link_libraries(app-lib
tinyexpr
qoi)
if(ENABLE_WEBP AND WEBP_FOUND)
target_link_libraries(app-lib ${WEBP_LIBRARIES})
target_include_directories(app-lib PUBLIC ${WEBP_INCLUDE_DIR})
endif()
if(FREETYPE_FOUND)
target_link_libraries(app-lib ${FREETYPE_LIBRARIES})
target_include_directories(app-lib PUBLIC ${FREETYPE_INCLUDE_DIRS})
endif()
if(HARFBUZZ_FOUND)
target_link_libraries(app-lib ${HARFBUZZ_LIBRARIES})
target_include_directories(app-lib PUBLIC ${HARFBUZZ_INCLUDE_DIRS})
endif()
# Directory where generated files by "gen" utility will stay.
target_include_directories(app-lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})

View File

@ -861,7 +861,7 @@ int app_get_color_to_clear_layer(Layer* layer)
if (auto* colorBar = ColorBar::instance())
color = colorBar->getBgColor();
else
color = app::Color::fromRgb(0, 0, 0); // TODO get background color color from doc::Settings
color = Preferences::instance().colorBar.bgColor();
}
else { // All transparent layers are cleared with the mask color
color = app::Color::fromMask();

View File

@ -391,9 +391,8 @@ void AppMenus::reload()
#ifdef ENABLE_SCRIPTING
// Load scripts
ResourceFinder rf;
rf.includeUserDir("scripts/.");
rf.includeUserDir("scripts");
std::string scriptsDir = rf.getFirstOrCreateDefault();
scriptsDir = base::get_file_path(scriptsDir);
if (base::is_directory(scriptsDir)) {
loadScriptsSubmenu(scriptsMenu->getSubmenu(), scriptsDir, true);
}

View File

@ -23,6 +23,7 @@
#include "app/filename_formatter.h"
#include "app/restore_visible_layers.h"
#include "app/ui_context.h"
#include "app/util/layer_utils.h"
#include "base/convert_to.h"
#include "base/fs.h"
#include "base/split_string.h"
@ -43,17 +44,6 @@ namespace app {
namespace {
std::string get_layer_path(const Layer* layer)
{
std::string path;
for (; layer != layer->sprite()->root(); layer=layer->parent()) {
if (!path.empty())
path.insert(0, "/");
path.insert(0, layer->name());
}
return path;
}
bool match_path(const std::string& filter,
const std::string& layer_path,
const bool exclude)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -31,6 +31,8 @@
#ifdef ENABLE_SCRIPTING
#include "app/app.h"
#include "app/script/engine.h"
#include "app/script/script_input_chain.h"
#include "app/ui/input_chain.h"
#endif
#include <iostream>
@ -143,6 +145,10 @@ void DefaultCliDelegate::exportFiles(Context* ctx, DocExporter& exporter)
int DefaultCliDelegate::execScript(const std::string& filename,
const Params& params)
{
ScriptInputChain scriptInputChain;
if (!App::instance()->isGui()) {
App::instance()->inputChain().prioritize(&scriptInputChain, nullptr);
}
auto engine = App::instance()->scriptEngine();
if (!engine->evalUserFile(filename, params))
throw base::Exception("Error executing script %s", filename.c_str());

View File

@ -12,12 +12,18 @@
#include "app/cmd/flatten_layers.h"
#include "app/cmd/add_layer.h"
#include "app/cmd/add_cel.h"
#include "app/cmd/configure_background.h"
#include "app/cmd/copy_rect.h"
#include "app/cmd/move_layer.h"
#include "app/cmd/remove_layer.h"
#include "app/cmd/set_layer_flags.h"
#include "app/cmd/remove_cel.h"
#include "app/cmd/replace_image.h"
#include "app/cmd/set_layer_name.h"
#include "app/cmd/set_layer_opacity.h"
#include "app/cmd/set_layer_blend_mode.h"
#include "app/cmd/set_cel_opacity.h"
#include "app/cmd/set_cel_zindex.h"
#include "app/cmd/set_cel_position.h"
#include "app/cmd/unlink_cel.h"
#include "app/doc.h"
#include "app/i18n/strings.h"
@ -34,10 +40,10 @@ namespace cmd {
FlattenLayers::FlattenLayers(doc::Sprite* sprite,
const doc::SelectedLayers& layers0,
const bool newBlend)
const Options options)
: WithSprite(sprite)
{
m_newBlendMethod = newBlend;
m_options = options;
doc::SelectedLayers layers(layers0);
layers.removeChildrenIfParentIsSelected();
@ -66,8 +72,27 @@ void FlattenLayers::onExecute()
if (list.empty())
return; // Do nothing
// Set the drawable area to a union of all cel bounds
// when this option is enabled
ImageSpec spec = sprite->spec();
gfx::Rect area;
if (m_options.dynamicCanvas) {
for (frame_t frame(0); frame<sprite->totalFrames(); ++frame) {
for (Layer* layer : layers) {
Cel* cel = layer->cel(frame);
if (cel)
area |= cel->bounds();
}
}
spec.setSize(area.size());
}
// Otherwise use the sprite's bounds
else {
area.setSize(spec.size());
}
// Create a temporary image.
ImageRef image(Image::create(sprite->spec()));
ImageRef image(Image::create(spec));
LayerImage* flatLayer; // The layer onto which everything will be flattened.
color_t bgcolor; // The background color to use for flatLayer.
@ -78,6 +103,12 @@ void FlattenLayers::onExecute()
// There exists a visible background layer, so we will flatten onto that.
bgcolor = doc->bgColor(flatLayer);
}
// Get bottom layer when merging layers in-place, but only if
// we are not flattening into the background layer
else if (m_options.inplace) {
flatLayer = static_cast<LayerImage*>(list.front());
bgcolor = sprite->transparentColor();
}
else {
// Create a new transparent layer to flatten everything onto it.
flatLayer = new LayerImage(sprite);
@ -88,7 +119,7 @@ void FlattenLayers::onExecute()
}
render::Render render;
render.setNewBlend(m_newBlendMethod);
render.setNewBlend(m_options.newBlendMethod);
render.setBgOptions(render::BgOptions::MakeNone());
{
@ -97,44 +128,97 @@ void FlattenLayers::onExecute()
RestoreVisibleLayers restore;
restore.showSelectedLayers(sprite, layers);
// Map draw area to image coords
const gfx::ClipF area_to_image(0, 0, area);
// Copy all frames to the background.
for (frame_t frame(0); frame<sprite->totalFrames(); ++frame) {
// Clear the image and render this frame.
clear_image(image.get(), bgcolor);
render.renderSprite(image.get(), sprite, frame);
render.renderSprite(image.get(), sprite, frame, area_to_image);
// TODO Keep cel links when possible
// Get exact bounds for rendered frame
gfx::Rect bounds = image->bounds();
const bool shrink = doc::algorithm::shrink_bounds(
image.get(), image->maskColor(), nullptr,
image->bounds(), bounds);
ImageRef cel_image;
// Skip when fully transparent
Cel* cel = flatLayer->cel(frame);
if (!shrink) {
if (!newFlatLayer && cel)
executeAndAdd(new cmd::RemoveCel(cel));
continue;
}
// Apply shrunk bounds to new image
const ImageRef new_image(doc::crop_image(
image.get(), bounds, image->maskColor()));
// Replace image on existing cel
if (cel) {
// TODO Keep cel links when possible
if (cel->links())
executeAndAdd(new cmd::UnlinkCel(cel));
cel_image = cel->imageRef();
const ImageRef cel_image = cel->imageRef();
ASSERT(cel_image);
executeAndAdd(
new cmd::CopyRect(cel_image.get(), image.get(),
gfx::Clip(0, 0, image->bounds())));
// Reset cel properties when flattening in-place
if (!newFlatLayer) {
if (cel->opacity() != 255)
executeAndAdd(new cmd::SetCelOpacity(cel, 255));
if (cel->zIndex() != 0)
executeAndAdd(new cmd::SetCelZIndex(cel, 0));
executeAndAdd(new cmd::SetCelPosition(cel,
area.x+bounds.x, area.y+bounds.y));
}
// Modify destination cel
executeAndAdd(new cmd::ReplaceImage(sprite, cel_image, new_image));
}
// Add new cel on null
else {
gfx::Rect bounds(image->bounds());
if (doc::algorithm::shrink_bounds(
image.get(), image->maskColor(), nullptr, bounds)) {
cel_image.reset(
doc::crop_image(image.get(), bounds, image->maskColor()));
cel = new Cel(frame, cel_image);
cel->setPosition(bounds.origin());
cel = new Cel(frame, new_image);
cel->setPosition(area.x+bounds.x, area.y+bounds.y);
// No need to undo adding this cel when flattening onto
// a new layer, as the layer itself would be destroyed,
// hence the lack of a command
if (newFlatLayer) {
flatLayer->addCel(cel);
}
else {
executeAndAdd(new cmd::AddCel(flatLayer, cel));
}
}
}
}
// Notify observers when merging down
if (m_options.mergeDown)
doc->notifyLayerMergedDown(list.back(), flatLayer);
// Add new flatten layer
if (newFlatLayer)
executeAndAdd(new cmd::AddLayer(list.front()->parent(), flatLayer, list.front()));
if (newFlatLayer) {
executeAndAdd(new cmd::AddLayer(
list.front()->parent(), flatLayer, list.front()));
}
// Reset layer properties when flattening in-place
else {
if (flatLayer->opacity() != 255)
executeAndAdd(new cmd::SetLayerOpacity(flatLayer, 255));
if (flatLayer->blendMode() != doc::BlendMode::NORMAL)
executeAndAdd(new cmd::SetLayerBlendMode(
flatLayer, doc::BlendMode::NORMAL));
}
// Delete flattened layers.
for (Layer* layer : layers) {

View File

@ -20,16 +20,31 @@ namespace cmd {
class FlattenLayers : public CmdSequence
, public WithSprite {
public:
struct Options {
bool newBlendMethod: 1;
bool inplace: 1;
bool mergeDown: 1;
bool dynamicCanvas: 1;
Options():
newBlendMethod(false),
inplace(false),
mergeDown(false),
dynamicCanvas(false) {
}
};
FlattenLayers(doc::Sprite* sprite,
const doc::SelectedLayers& layers,
const bool newBlendMethod);
const Options options);
protected:
void onExecute() override;
private:
doc::ObjectIds m_layerIds;
bool m_newBlendMethod;
Options m_options;
};
} // namespace cmd

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -15,6 +15,7 @@
#include "app/cmd/replace_image.h"
#include "app/cmd/set_cel_opacity.h"
#include "app/cmd/set_palette.h"
#include "app/cmd/set_transparent_color.h"
#include "app/doc.h"
#include "app/doc_event.h"
#include "doc/cel.h"
@ -73,7 +74,8 @@ SetPixelFormat::SetPixelFormat(Sprite* sprite,
const render::Dithering& dithering,
const doc::RgbMapAlgorithm mapAlgorithm,
doc::rgba_to_graya_func toGray,
render::TaskDelegate* delegate)
render::TaskDelegate* delegate,
const FitCriteria fitCriteria)
: WithSprite(sprite)
, m_oldFormat(sprite->pixelFormat())
, m_newFormat(newFormat)
@ -108,7 +110,8 @@ SetPixelFormat::SetPixelFormat(Sprite* sprite,
cel->layer()->isBackground(),
mapAlgorithm,
toGray,
&superDel);
&superDel,
fitCriteria);
superDel.nextImage();
}
@ -128,20 +131,45 @@ SetPixelFormat::SetPixelFormat(Sprite* sprite,
false, // TODO is background? it depends of the layer where this tileset is used
mapAlgorithm,
toGray,
&superDel);
&superDel,
fitCriteria);
}
superDel.nextImage();
}
}
}
// Set all cels opacity to 100% if we are converting to indexed.
// TODO remove this
// By default, when converting to RGB or grayscale, the mask color
// is always 0.
int newMaskIndex = 0;
if (newFormat == IMAGE_INDEXED) {
// Set all cels opacity to 100% if we are converting to indexed.
// TODO remove this (?)
for (Cel* cel : sprite->uniqueCels()) {
if (cel->opacity() < 255)
m_seq.add(new cmd::SetCelOpacity(cel, 255));
m_pre.add(new cmd::SetCelOpacity(cel, 255));
}
// When converting to indexed mode the mask color depends if the
// palette includes a fully transparent entry.
newMaskIndex = sprite->palette(0)->findMaskColor();
if (newMaskIndex < 0)
newMaskIndex = 0;
// We change the transparent color after (m_post) changing the
// color mode (when we are already in indexed mode).
if (newMaskIndex != sprite->transparentColor())
m_post.add(new cmd::SetTransparentColor(sprite, newMaskIndex));
}
else if (m_oldFormat == IMAGE_INDEXED) {
// We change the transparent color before (m_pre) changing the
// color mode (when we are still in indexed mode).
if (newMaskIndex != sprite->transparentColor())
m_pre.add(new cmd::SetTransparentColor(sprite, newMaskIndex));
}
else {
// RGB <-> Grayscale
ASSERT(sprite->transparentColor() == 0);
}
// When we are converting to grayscale color mode, we've to destroy
@ -152,30 +180,33 @@ SetPixelFormat::SetPixelFormat(Sprite* sprite,
PalettesList palettes = sprite->getPalettes();
for (Palette* pal : palettes)
if (pal->frame() != 0)
m_seq.add(new cmd::RemovePalette(sprite, pal));
m_pre.add(new cmd::RemovePalette(sprite, pal));
std::unique_ptr<Palette> graypal(Palette::createGrayscale());
if (*graypal != *sprite->palette(0))
m_seq.add(new cmd::SetPalette(sprite, 0, graypal.get()));
m_pre.add(new cmd::SetPalette(sprite, 0, graypal.get()));
}
}
void SetPixelFormat::onExecute()
{
m_seq.execute(context());
m_pre.execute(context());
setFormat(m_newFormat);
m_post.execute(context());
}
void SetPixelFormat::onUndo()
{
m_seq.undo();
m_post.undo();
setFormat(m_oldFormat);
m_pre.undo();
}
void SetPixelFormat::onRedo()
{
m_seq.redo();
m_pre.redo();
setFormat(m_newFormat);
m_post.redo();
}
void SetPixelFormat::setFormat(PixelFormat format)
@ -183,12 +214,6 @@ void SetPixelFormat::setFormat(PixelFormat format)
Sprite* sprite = this->sprite();
sprite->setPixelFormat(format);
if (format == IMAGE_INDEXED) {
int maskIndex = sprite->palette(0)->findMaskColor();
sprite->setTransparentColor(maskIndex == -1 ? 0 : maskIndex);
}
else
sprite->setTransparentColor(0);
sprite->incrementVersion();
// Regenerate extras
@ -208,7 +233,8 @@ void SetPixelFormat::convertImage(doc::Sprite* sprite,
const bool isBackground,
const doc::RgbMapAlgorithm mapAlgorithm,
doc::rgba_to_graya_func toGray,
render::TaskDelegate* delegate)
render::TaskDelegate* delegate,
const doc::FitCriteria fitCriteria)
{
ASSERT(oldImage);
ASSERT(oldImage->pixelFormat() != IMAGE_TILEMAP);
@ -218,7 +244,10 @@ void SetPixelFormat::convertImage(doc::Sprite* sprite,
RgbMap* rgbmap;
int newMaskIndex = (isBackground ? -1 : 0);
if (m_newFormat == IMAGE_INDEXED) {
rgbmap = sprite->rgbMap(frame, sprite->rgbMapForSprite(), mapAlgorithm);
rgbmap = sprite->rgbMap(frame,
sprite->rgbMapForSprite(),
mapAlgorithm,
fitCriteria);
if (m_oldFormat == IMAGE_INDEXED)
newMaskIndex = sprite->transparentColor();
else
@ -238,7 +267,7 @@ void SetPixelFormat::convertImage(doc::Sprite* sprite,
toGray,
delegate));
m_seq.add(new cmd::ReplaceImage(sprite, oldImage, newImage));
m_pre.add(new cmd::ReplaceImage(sprite, oldImage, newImage));
}
} // namespace cmd

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -12,6 +12,7 @@
#include "app/cmd/with_sprite.h"
#include "app/cmd_sequence.h"
#include "doc/color.h"
#include "doc/fit_criteria.h"
#include "doc/frame.h"
#include "doc/image_ref.h"
#include "doc/pixel_format.h"
@ -37,14 +38,15 @@ namespace cmd {
const render::Dithering& dithering,
const doc::RgbMapAlgorithm mapAlgorithm,
doc::rgba_to_graya_func toGray,
render::TaskDelegate* delegate);
render::TaskDelegate* delegate,
const doc::FitCriteria fitCriteria);
protected:
void onExecute() override;
void onUndo() override;
void onRedo() override;
size_t onMemSize() const override {
return sizeof(*this) + m_seq.memSize();
return sizeof(*this) + m_pre.memSize() + m_post.memSize();
}
private:
@ -56,11 +58,13 @@ namespace cmd {
const bool isBackground,
const doc::RgbMapAlgorithm mapAlgorithm,
doc::rgba_to_graya_func toGray,
render::TaskDelegate* delegate);
render::TaskDelegate* delegate,
const doc::FitCriteria fitCriteria = doc::FitCriteria::DEFAULT);
doc::PixelFormat m_oldFormat;
doc::PixelFormat m_newFormat;
CmdSequence m_seq;
CmdSequence m_pre;
CmdSequence m_post;
};
} // namespace cmd

View File

@ -63,7 +63,7 @@ void CancelCommand::onExecute(Context* context)
case All:
// TODO should the ContextBar be a InputChainElement to intercept onCancel()?
// Discard brush
{
if (context->isUIAvailable()) {
Command* discardBrush = Commands::instance()->byId(
CommandId::DiscardBrush());
context->executeCommand(discardBrush);

View File

@ -23,6 +23,7 @@
#include "app/modules/palettes.h"
#include "app/sprite_job.h"
#include "app/transaction.h"
#include "app/ui/best_fit_criteria_selector.h"
#include "app/ui/dithering_selector.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/editor_render.h"
@ -69,7 +70,6 @@ public:
const doc::frame_t frame,
const doc::PixelFormat pixelFormat,
const render::Dithering& dithering,
const doc::RgbMapAlgorithm rgbMapAlgorithm,
const gen::ToGrayAlgorithm toGray,
const gfx::Point& pos,
const bool newBlend)
@ -83,13 +83,11 @@ public:
sprite, frame,
pixelFormat,
dithering,
rgbMapAlgorithm,
toGray,
newBlend]() { // Copy the matrix
run(sprite, frame,
pixelFormat,
dithering,
rgbMapAlgorithm,
toGray,
newBlend);
})
@ -114,7 +112,6 @@ private:
const doc::frame_t frame,
const doc::PixelFormat pixelFormat,
const render::Dithering& dithering,
const doc::RgbMapAlgorithm rgbMapAlgorithm,
const gen::ToGrayAlgorithm toGray,
const bool newBlend) {
doc::ImageRef tmp(
@ -136,9 +133,7 @@ private:
m_image.get(),
pixelFormat,
dithering,
sprite->rgbMap(frame,
sprite->rgbMapForSprite(),
rgbMapAlgorithm),
sprite->rgbMap(frame),
sprite->palette(frame),
(sprite->backgroundLayer() != nullptr),
0,
@ -193,6 +188,7 @@ public:
, m_selectedItem(nullptr)
, m_ditheringSelector(nullptr)
, m_mapAlgorithmSelector(nullptr)
, m_bestFitCriteriaSelector(nullptr)
, m_imageJustCreated(true)
{
const auto& pref = Preferences::instance();
@ -219,6 +215,9 @@ public:
m_mapAlgorithmSelector = new RgbMapAlgorithmSelector;
m_mapAlgorithmSelector->setExpansive(true);
m_bestFitCriteriaSelector = new BestFitCriteriaSelector;
m_bestFitCriteriaSelector->setExpansive(true);
// Select default dithering method
{
int index = m_ditheringSelector->findItemIndex(
@ -230,8 +229,12 @@ public:
// Select default RgbMap algorithm
m_mapAlgorithmSelector->algorithm(pref.quantization.rgbmapAlgorithm());
// Select default best fit criteria
m_bestFitCriteriaSelector->criteria(pref.quantization.fitCriteria());
ditheringPlaceholder()->addChild(m_ditheringSelector);
rgbmapAlgorithmPlaceholder()->addChild(m_mapAlgorithmSelector);
bestFitCriteriaPlaceholder()->addChild(m_bestFitCriteriaSelector);
const bool adv = pref.quantization.advanced();
advancedCheck()->setSelected(adv);
@ -240,6 +243,7 @@ public:
// Signals
m_ditheringSelector->Change.connect([this]{ onIndexParamChange(); });
m_mapAlgorithmSelector->Change.connect([this]{ onIndexParamChange(); });
m_bestFitCriteriaSelector->Change.connect([this]{ onIndexParamChange(); });
factor()->Change.connect([this]{ onIndexParamChange(); });
advancedCheck()->Click.connect(
@ -301,6 +305,13 @@ public:
return doc::RgbMapAlgorithm::DEFAULT;
}
doc::FitCriteria fitCriteria() const {
if (m_bestFitCriteriaSelector)
return m_bestFitCriteriaSelector->criteria();
else
return doc::FitCriteria::DEFAULT;
}
gen::ToGrayAlgorithm toGray() const {
static_assert(
int(gen::ToGrayAlgorithm::LUMA) == 0 &&
@ -331,7 +342,7 @@ public:
}
}
if (m_mapAlgorithmSelector)
if (m_mapAlgorithmSelector || m_bestFitCriteriaSelector)
pref.quantization.advanced(advancedCheck()->isSelected());
}
@ -396,6 +407,12 @@ private:
visibleBounds.origin(),
doc::BlendMode::SRC);
m_editor->sprite()->rgbMap(
0,
m_editor->sprite()->rgbMapForSprite(),
rgbMapAlgorithm(),
fitCriteria());
m_editor->invalidate();
progress()->setValue(0);
progress()->setVisible(false);
@ -408,7 +425,6 @@ private:
m_editor->frame(),
dstPixelFormat,
dithering(),
rgbMapAlgorithm(),
toGray(),
visibleBounds.origin(),
Preferences::instance().experimental.newBlend()));
@ -463,6 +479,7 @@ private:
ConversionItem* m_selectedItem;
DitheringSelector* m_ditheringSelector;
RgbMapAlgorithmSelector* m_mapAlgorithmSelector;
BestFitCriteriaSelector* m_bestFitCriteriaSelector;
bool m_imageJustCreated;
};
@ -485,6 +502,7 @@ private:
doc::PixelFormat m_format;
render::Dithering m_dithering;
doc::RgbMapAlgorithm m_rgbmap;
doc::FitCriteria m_fitCriteria = FitCriteria::DEFAULT;
gen::ToGrayAlgorithm m_toGray;
};
@ -624,7 +642,10 @@ void ChangePixelFormatCommand::onExecute(Context* context)
{
bool flatten = false;
if (context->isUIAvailable() && m_showDlg) {
if (!context->isUIAvailable()) {
// do nothing
}
else if (m_showDlg) {
ColorModeWindow window(Editor::activeEditor());
window.remapWindow();
@ -640,11 +661,18 @@ void ChangePixelFormatCommand::onExecute(Context* context)
m_format = window.pixelFormat();
m_dithering = window.dithering();
m_rgbmap = window.rgbMapAlgorithm();
m_fitCriteria = window.fitCriteria();
m_toGray = window.toGray();
flatten = window.flattenEnabled();
window.saveOptions();
}
else {
if (m_format == IMAGE_INDEXED) {
m_rgbmap = Preferences::instance().quantization.rgbmapAlgorithm();
m_fitCriteria = Preferences::instance().quantization.fitCriteria();
}
}
// No conversion needed
Doc* doc = context->activeDocument();
@ -665,10 +693,13 @@ void ChangePixelFormatCommand::onExecute(Context* context)
if (flatten) {
Tx tx(Tx::LockDoc, context, doc);
const bool newBlend = Preferences::instance().experimental.newBlend();
cmd::FlattenLayers::Options options;
options.newBlendMethod = newBlend;
SelectedLayers selLayers;
for (auto layer : sprite->root()->layers())
selLayers.insert(layer);
tx(new cmd::FlattenLayers(sprite, selLayers, newBlend));
tx(new cmd::FlattenLayers(sprite, selLayers, options));
}
job.startJobWithCallback(
@ -679,7 +710,8 @@ void ChangePixelFormatCommand::onExecute(Context* context)
m_dithering,
m_rgbmap,
get_gray_func(m_toGray),
&job)); // SpriteJob is a render::TaskDelegate
&job,
m_fitCriteria)); // SpriteJob is a render::TaskDelegate
});
job.waitJob();
}

View File

@ -32,6 +32,7 @@
#include "app/ui/optional_alert.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/util/layer_utils.h"
#include "base/convert_to.h"
#include "base/fs.h"
#include "base/string.h"
@ -148,6 +149,17 @@ void destroy_doc(Context* ctx, Doc* doc)
}
}
void insert_layers_to_selected_layers(Layer* layer, SelectedLayers& selectedLayers)
{
if (layer->isGroup()) {
auto children = static_cast<LayerGroup*>(layer)->layers();
for (auto child : children)
insert_layers_to_selected_layers(child, selectedLayers);
}
else
selectedLayers.insert(layer);
}
Doc* generate_sprite_sheet_from_params(
DocExporter& exporter,
Context* ctx,
@ -206,11 +218,14 @@ Doc* generate_sprite_sheet_from_params(
if (layerName != kSelectedLayers) {
// TODO add a getLayerByName
int i = sprite->allLayersCount();
for (const Layer* layer : sprite->allLayers()) {
for (Layer* layer : sprite->allLayers()) {
i--;
if (layer->name() == layerName && (layerIndex == -1 ||
layerIndex == i)) {
selLayers.insert(const_cast<Layer*>(layer));
if (get_layer_path(layer) == layerName &&
(layerIndex == -1 || layerIndex == i)) {
if (layer->isGroup())
insert_layers_to_selected_layers(layer, selLayers);
else
selLayers.insert(layer);
break;
}
}

View File

@ -81,9 +81,11 @@ void FlattenLayersCommand::onExecute(Context* context)
}
}
const bool newBlend = Preferences::instance().experimental.newBlend();
cmd::FlattenLayers::Options options;
options.newBlendMethod = newBlend;
tx(new cmd::FlattenLayers(sprite,
range.selectedLayers(),
newBlend));
options));
tx.commit();
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -9,6 +10,7 @@
#pragma once
#include "app/commands/command.h"
#include "app/commands/params.h"
#include "doc/algorithm/flip_type.h"
namespace app {
@ -24,6 +26,9 @@ namespace app {
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override {
return !params.empty();
}
private:
bool m_flipMask;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -14,6 +15,7 @@
#include "app/context.h"
#include "app/context_access.h"
#include "app/doc_api.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/tx.h"
#include "base/convert_to.h"
@ -35,6 +37,7 @@ protected:
void onLoadParams(const Params& params) override;
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
private:
enum Target {
@ -137,6 +140,17 @@ void FramePropertiesCommand::onExecute(Context* context)
}
}
std::string FramePropertiesCommand::onGetFriendlyName() const
{
switch (m_target) {
case CURRENT_RANGE:
return Strings::commands_FrameProperties_Current() ;
case ALL_FRAMES:
return Strings::commands_FrameProperties_All();
}
return Command::onGetFriendlyName();
}
Command* CommandFactory::createFramePropertiesCommand()
{
return new FramePropertiesCommand;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of

View File

@ -605,7 +605,8 @@ private:
if (key->type() == KeyType::Tool ||
key->type() == KeyType::Quicktool ||
key->type() == KeyType::WheelAction ||
key->type() == KeyType::DragAction) {
key->type() == KeyType::DragAction ||
key->isListed()) {
continue;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -24,6 +24,10 @@ public:
protected:
void onLoadParams(const Params& params) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override {
return !params.get("path").empty();
}
private:
enum Type { Url };
@ -59,6 +63,11 @@ void LaunchCommand::onExecute(Context* context)
}
}
std::string LaunchCommand::onGetFriendlyName() const
{
return Command::onGetFriendlyName() + ": " + m_path;
}
Command* CommandFactory::createLaunchCommand()
{
return new LaunchCommand;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2020-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of

View File

@ -32,6 +32,7 @@ public:
protected:
void onLoadParams(const Params& params) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
private:
std::string m_preset;
@ -88,6 +89,14 @@ void LoadPaletteCommand::onExecute(Context* context)
context->executeCommand(cmd);
}
std::string LoadPaletteCommand::onGetFriendlyName() const
{
std::string name = Command::onGetFriendlyName();
if (m_preset == "default")
name = Strings::commands_LoadDefaultPalette();
return name;
}
Command* CommandFactory::createLoadPaletteCommand()
{
return new LoadPaletteCommand;

View File

@ -10,23 +10,18 @@
#endif
#include "app/app.h"
#include "app/cmd/add_cel.h"
#include "app/cmd/replace_image.h"
#include "app/cmd/set_cel_position.h"
#include "app/cmd/unlink_cel.h"
#include "app/pref/preferences.h"
#include "app/cmd/flatten_layers.h"
#include "app/commands/command.h"
#include "app/context_access.h"
#include "app/doc.h"
#include "app/doc_api.h"
#include "app/doc_range.h"
#include "app/modules/gui.h"
#include "app/tx.h"
#include "doc/blend_internals.h"
#include "doc/cel.h"
#include "doc/image.h"
#include "doc/layer.h"
#include "doc/primitives.h"
#include "doc/sprite.h"
#include "render/rasterize.h"
#include "ui/ui.h"
namespace app {
@ -81,86 +76,18 @@ void MergeDownLayerCommand::onExecute(Context* context)
Tx tx(writer, friendlyName(), ModifyDocument);
for (frame_t frpos = 0; frpos<sprite->totalFrames(); ++frpos) {
// Get frames
Cel* src_cel = src_layer->cel(frpos);
Cel* dst_cel = dst_layer->cel(frpos);
DocRange range;
range.selectLayer(writer.layer());
range.selectLayer(dst_layer);
// Get images
Image* src_image;
if (src_cel != NULL)
src_image = src_cel->image();
else
src_image = NULL;
ImageRef dst_image;
if (dst_cel)
dst_image = dst_cel->imageRef();
// With source image?
if (src_image) {
int t;
int opacity;
opacity = MUL_UN8(src_cel->opacity(), src_layer->opacity(), t);
// No destination image
if (!dst_image) { // Only a transparent layer can have a null cel
// Copy this cel to the destination layer...
// Creating a copy of the image
dst_image.reset(
render::rasterize_with_cel_bounds(src_cel));
// Creating a copy of the cell
dst_cel = new Cel(frpos, dst_image);
dst_cel->setPosition(src_cel->x(), src_cel->y());
dst_cel->setOpacity(opacity);
tx(new cmd::AddCel(dst_layer, dst_cel));
}
// With destination
else {
gfx::Rect bounds;
// Merge down in the background layer
if (dst_layer->isBackground()) {
bounds = sprite->bounds();
}
// Merge down in a transparent layer
else {
bounds = src_cel->bounds().createUnion(dst_cel->bounds());
}
doc::color_t bgcolor = app_get_color_to_clear_layer(dst_layer);
ImageRef new_image(doc::crop_image(
dst_image.get(),
bounds.x-dst_cel->x(),
bounds.y-dst_cel->y(),
bounds.w, bounds.h, bgcolor));
// Draw src_cel on new_image
render::rasterize(
new_image.get(), src_cel,
-bounds.x, -bounds.y, false);
// First unlink the dst_cel
if (dst_cel->links())
tx(new cmd::UnlinkCel(dst_cel));
// Then modify the dst_cel
tx(new cmd::SetCelPosition(dst_cel,
bounds.x, bounds.y));
tx(new cmd::ReplaceImage(sprite,
dst_cel->imageRef(), new_image));
}
}
}
document->notifyLayerMergedDown(src_layer, dst_layer);
document->getApi(tx).removeLayer(src_layer); // src_layer is deleted inside removeLayer()
const bool newBlend = Preferences::instance().experimental.newBlend();
cmd::FlattenLayers::Options options;
options.newBlendMethod = newBlend;
options.inplace = true;
options.mergeDown = true;
options.dynamicCanvas = true;
tx(new cmd::FlattenLayers(sprite, range.selectedLayers(), options));
tx.commit();
update_screen_for_document(document);

View File

@ -45,6 +45,7 @@ protected:
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
private:
std::string getActionName() const;

View File

@ -263,6 +263,11 @@ void NewFileCommand::onExecute(Context* ctx)
else
layer->setName(fmt::format("{} {}", Strings::commands_NewLayer_Layer(), 1));
}
if (sprite->pixelFormat() == IMAGE_INDEXED) {
sprite->rgbMap(0, Sprite::RgbMapFor(!layer->isBackground()),
Preferences::instance().quantization.rgbmapAlgorithm(),
Preferences::instance().quantization.fitCriteria());
}
// Show the sprite to the user
std::unique_ptr<Doc> doc(new Doc(sprite.get()));

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2016-2017 David Capello
//
// This program is distributed under the terms of
@ -23,6 +24,8 @@ public:
protected:
void onLoadParams(const Params& params) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
private:
std::string m_filename;
@ -43,6 +46,11 @@ void OpenBrowserCommand::onExecute(Context* context)
App::instance()->mainWindow()->showBrowser(m_filename);
}
std::string OpenBrowserCommand::onGetFriendlyName() const
{
return Command::onGetFriendlyName() + ": " + m_filename;
}
Command* CommandFactory::createOpenBrowserCommand()
{
return new OpenBrowserCommand;

View File

@ -269,6 +269,21 @@ void OpenFileCommand::onExecute(Context* context)
}
}
std::string OpenFileCommand::onGetFriendlyName() const
{
// TO DO: would be better to show the last part of the path
// via text size hint instead of a fixed number of chars.
auto uiScale = Preferences::instance().general.uiScale();
auto scScale = Preferences::instance().general.screenScale();
int pos(68.0 / double(uiScale) / double(scScale));
return Command::onGetFriendlyName().append(
(m_filename.empty() ?
"" :
(": " + (m_filename.size() >= pos ?
m_filename.substr(m_filename.size() - pos, pos) :
m_filename))));
}
Command* CommandFactory::createOpenFileCommand()
{
return new OpenFileCommand;

View File

@ -10,6 +10,7 @@
#pragma once
#include "app/commands/command.h"
#include "app/commands/params.h"
#include "app/pref/preferences.h"
#include "base/paths.h"
@ -32,6 +33,7 @@ namespace app {
protected:
void onLoadParams(const Params& params) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
private:
std::string m_filename;

View File

@ -25,7 +25,9 @@
#include "app/pref/preferences.h"
#include "app/recent_files.h"
#include "app/resource_finder.h"
#include "app/tools/tool_box.h"
#include "app/tx.h"
#include "app/ui/best_fit_criteria_selector.h"
#include "app/ui/color_button.h"
#include "app/ui/main_window.h"
#include "app/ui/pref_widget.h"
@ -272,14 +274,7 @@ public:
// Theme variants
fillThemeVariants();
// Default extension to save files
fillExtensionsCombobox(defaultExtension(), m_pref.saveFile.defaultExtension());
fillExtensionsCombobox(exportImageDefaultExtension(), m_pref.exportFile.imageDefaultExtension());
fillExtensionsCombobox(exportAnimationDefaultExtension(), m_pref.exportFile.animationDefaultExtension());
fillExtensionsCombobox(exportSpriteSheetDefaultExtension(), m_pref.spriteSheet.defaultExtension());
// Number of recent items
recentFiles()->setValue(m_pref.general.recentItems());
// Recent files
clearRecentFiles()->Click.connect([this]{ onClearRecentFiles(); });
// Template item for active display color profiles
@ -295,31 +290,24 @@ public:
if (cs->gfxColorSpace()->type() != gfx::ColorSpace::None)
workingRgbCs()->addItem(new ColorSpaceItem(cs));
}
updateColorProfileControls(m_pref.color.manage(),
m_pref.color.windowProfile(),
m_pref.color.windowProfileName(),
m_pref.color.workingRgbSpace(),
m_pref.color.filesWithProfile(),
m_pref.color.missingProfile());
}
// Alerts
openSequence()->setSelectedItemIndex(int(m_pref.openFile.openSequence()));
resetAlerts()->Click.connect([this]{ onResetAlerts(); });
// Cursor
paintingCursorType()->setSelectedItemIndex(int(m_pref.cursor.paintingCursorType()));
cursorColor()->setColor(m_pref.cursor.cursorColor());
if (cursorColor()->getColor().getType() == app::Color::MaskType) {
cursorColorType()->setSelectedItemIndex(0);
cursorColor()->setVisible(false);
}
else {
cursorColorType()->setSelectedItemIndex(1);
cursorColor()->setVisible(true);
}
cursorColorType()->Change.connect([this]{ onCursorColorType(); });
nativeCursor()->Click.connect([this]{ onNativeCursorChange(); });
// Dialogs
showAsepriteFileDialog()->Click.connect([this]{
nativeFileDialog()->setSelected(
!showAsepriteFileDialog()->isSelected());
});
nativeFileDialog()->Click.connect([this]{
showAsepriteFileDialog()->setSelected(
!nativeFileDialog()->isSelected());
});
// Grid
gridW()->Leave.connect([this] {
@ -333,27 +321,10 @@ public:
gridH()->setText("1");
});
// Brush preview
brushPreview()->setSelectedItemIndex(
(int)m_pref.cursor.brushPreview());
// Guide colors
layerEdgesColor()->setColor(m_pref.guides.layerEdgesColor());
autoGuidesColor()->setColor(m_pref.guides.autoGuidesColor());
// Slices default color
defaultSliceColor()->setColor(m_pref.slices.defaultColor());
// Timeline
firstFrame()->setTextf("%d", m_globPref.timeline.firstFrame());
resetTimelineSel()->Click.connect([this]{ onResetTimelineSel(); });
// Others
if (m_pref.general.expandMenubarOnMouseover())
expandMenubarOnMouseover()->setSelected(true);
if (m_pref.general.dataRecovery())
enableDataRecovery()->setSelected(true);
enableDataRecovery()->Click.connect(
[this](){
const bool state = enableDataRecovery()->isSelected();
@ -362,100 +333,10 @@ public:
keepEditedSpriteDataFor()->setEnabled(state);
});
if (m_pref.general.dataRecovery() &&
m_pref.general.keepEditedSpriteData())
keepEditedSpriteData()->setSelected(true);
else if (!m_pref.general.dataRecovery()) {
keepEditedSpriteData()->setEnabled(false);
keepEditedSpriteDataFor()->setEnabled(false);
}
if (m_pref.general.keepClosedSpriteOnMemory())
keepClosedSpriteOnMemory()->setSelected(true);
if (m_pref.general.showFullPath())
showFullPath()->setSelected(true);
dataRecoveryPeriod()->setSelectedItemIndex(
dataRecoveryPeriod()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.dataRecoveryPeriod())));
keepEditedSpriteDataFor()->setSelectedItemIndex(
keepEditedSpriteDataFor()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.keepEditedSpriteDataFor())));
keepClosedSpriteOnMemoryFor()->setSelectedItemIndex(
keepClosedSpriteOnMemoryFor()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.keepClosedSpriteOnMemoryFor())));
if (m_pref.editor.zoomFromCenterWithWheel())
zoomFromCenterWithWheel()->setSelected(true);
if (m_pref.editor.zoomFromCenterWithKeys())
zoomFromCenterWithKeys()->setSelected(true);
if (m_pref.selection.autoOpaque())
autoOpaque()->setSelected(true);
if (m_pref.selection.keepSelectionAfterClear())
keepSelectionAfterClear()->setSelected(true);
if (m_pref.selection.autoShowSelectionEdges())
autoShowSelectionEdges()->setSelected(true);
if (m_pref.selection.moveEdges())
moveEdges()->setSelected(true);
if (m_pref.selection.modifiersDisableHandles())
modifiersDisableHandles()->setSelected(true);
if (m_pref.selection.moveOnAddMode())
moveOnAddMode()->setSelected(true);
// If the platform supports native cursors...
if (m_system->hasCapability(os::Capabilities::CustomMouseCursor)) {
if (m_pref.cursor.useNativeCursor())
nativeCursor()->setSelected(true);
nativeCursor()->Click.connect([this]{ onNativeCursorChange(); });
cursorScale()->setSelectedItemIndex(
cursorScale()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.cursor.cursorScale())));
}
else {
nativeCursor()->setEnabled(false);
}
onNativeCursorChange();
// "Show Aseprite file dialog" option is the inverse of the old
// experimental "use native file dialog" option
showAsepriteFileDialog()->setSelected(
!m_pref.experimental.useNativeFileDialog());
showAsepriteFileDialog()->Click.connect([this]{
nativeFileDialog()->setSelected(
!showAsepriteFileDialog()->isSelected());
});
nativeFileDialog()->Click.connect([this]{
showAsepriteFileDialog()->setSelected(
!nativeFileDialog()->isSelected());
});
#ifdef LAF_WINDOWS // Show Tablet section on Windows
{
const os::TabletAPI tabletAPI = m_system->tabletOptions().api;
if (tabletAPI == os::TabletAPI::Wintab)
tabletApiWintabSystem()->setSelected(true);
else if (tabletAPI == os::TabletAPI::WintabPackets)
tabletApiWintabDirect()->setSelected(true);
else
tabletApiWindowsPointer()->setSelected(true);
onTabletAPIChange();
tabletApiWindowsPointer()->Click.connect([this](){ onTabletAPIChange(); });
tabletApiWintabSystem()->Click.connect([this](){ onTabletAPIChange(); });
tabletApiWintabDirect()->Click.connect([this](){ onTabletAPIChange(); });
}
tabletApiWindowsPointer()->Click.connect([this](){ onTabletAPIChange(); });
tabletApiWintabSystem()->Click.connect([this](){ onTabletAPIChange(); });
tabletApiWintabDirect()->Click.connect([this](){ onTabletAPIChange(); });
#else // For macOS and Linux
{
// Hide the "section_tablet" item (which is only for Windows at the moment)
@ -469,26 +350,11 @@ public:
}
#endif
if (m_pref.experimental.flashLayer())
flashLayer()->setSelected(true);
nonactiveLayersOpacity()->setValue(m_pref.experimental.nonactiveLayersOpacity());
rgbmapAlgorithmPlaceholder()->addChild(&m_rgbmapAlgorithmSelector);
m_rgbmapAlgorithmSelector.setExpansive(true);
m_rgbmapAlgorithmSelector.algorithm(m_pref.quantization.rgbmapAlgorithm());
if (m_pref.editor.showScrollbars())
showScrollbars()->setSelected(true);
if (m_pref.editor.autoScroll())
autoScroll()->setSelected(true);
if (m_pref.editor.straightLinePreview())
straightLinePreview()->setSelected(true);
if (m_pref.eyedropper.discardBrush())
discardBrush()->setSelected(true);
bestFitCriteriaPlaceholder()->addChild(&m_bestFitCriteriaSelector);
m_bestFitCriteriaSelector.setExpansive(true);
// Scope
bgScope()->addItem(Strings::options_bg_for_new_docs());
@ -503,9 +369,6 @@ public:
gridScope()->Change.connect([this]{ onChangeGridScope(); });
}
// Update the one/multiple window buttonset (and keep in on sync
// with the old/experimental checkbox)
uiWindows()->setSelectedItem(multipleWindows()->isSelected() ? 1: 0);
uiWindows()->ItemChange.connect([this]() {
multipleWindows()->setSelected(uiWindows()->selectedItem() == 1);
});
@ -513,29 +376,17 @@ public:
uiWindows()->setSelectedItem(multipleWindows()->isSelected() ? 1: 0);
});
// Scaling
selectScalingItems();
#ifdef ENABLE_DEVMODE // TODO enable this on Release when Aseprite supports
// GPU-acceleration properly
if (m_system->hasCapability(os::Capabilities::GpuAccelerationSwitch)) {
gpuAcceleration()->setSelected(m_pref.general.gpuAcceleration());
}
else
#endif
{
#ifndef ENABLE_DEVMODE // TODO enable this on Release when Aseprite supports
// GPU-acceleration properly
if (!m_system->hasCapability(os::Capabilities::GpuAccelerationSwitch))
gpuAcceleration()->setVisible(false);
}
#endif
// If the platform does support native menus, we show the option,
// in other case, the option doesn't make sense for this platform.
if (m_system->menus())
showMenuBar()->setSelected(m_pref.general.showMenuBar());
else
if (!m_system->menus())
showMenuBar()->setVisible(false);
showHome()->setSelected(m_pref.general.showHome());
// Editor sampling
samplingPlaceholder()->addChild(
m_samplingSelector = new SamplingSelector(
@ -563,7 +414,6 @@ public:
rightClickBehavior()->addItem(Strings::options_right_click_rectangular_marquee());
rightClickBehavior()->addItem(Strings::options_right_click_lasso());
rightClickBehavior()->addItem(Strings::options_right_click_select_layer_and_move());
rightClickBehavior()->setSelectedItemIndex((int)m_pref.editor.rightClickMode());
#ifndef __APPLE__ // Zoom sliding two fingers option only on macOS
slideZoom()->setVisible(false);
@ -601,11 +451,6 @@ public:
// Undo preferences
limitUndo()->Click.connect([this]{ onLimitUndoCheck(); });
limitUndo()->setSelected(m_pref.undo.sizeLimit() != 0);
onLimitUndoCheck();
undoGotoModified()->setSelected(m_pref.undo.gotoModified());
undoAllowNonlinearHistory()->setSelected(m_pref.undo.allowNonlinearHistory());
// Theme buttons
themeList()->Change.connect([this]{ onThemeChange(); });
@ -620,13 +465,27 @@ public:
uninstallExtension()->Click.connect([this]{ onUninstallExtension(); });
openExtensionFolder()->Click.connect([this]{ onOpenExtensionFolder(); });
// Reset checkboxes
// Prevent the user from clicking "Reset" if they don't have anything selected.
auto validateYesButton = [this] {
resetSelectedButton()->setEnabled(
defaultReset()->isSelected() || installedReset()->isSelected() ||
recentReset()->isSelected() || perfileReset()->isSelected() ||
toolsReset()->isSelected());
};
defaultReset()->Click.connect(validateYesButton);
installedReset()->Click.connect(validateYesButton);
recentReset()->Click.connect(validateYesButton);
perfileReset()->Click.connect(validateYesButton);
toolsReset()->Click.connect(validateYesButton);
resetSelectedButton()->Click.connect([this] { onResetDefault(); });
defaultReset()->setSelected(true);
// Apply button
buttonApply()->Click.connect([this]{ onApply(); });
onChangeBgScope();
onChangeGridScope();
sectionListbox()->selectIndex(m_curSection);
// Refill languages combobox when extensions are enabled/disabled
m_extLanguagesChanges =
App::instance()->extensions().LanguagesChange.connect(
@ -636,15 +495,178 @@ public:
m_extThemesChanges =
App::instance()->extensions().ThemesChange.connect(
[this]{ reloadThemes(); });
loadFromPreferences();
}
void loadFromPreferences() {
// Default extension to save files
fillExtensionsCombobox(defaultExtension(), m_pref.saveFile.defaultExtension());
fillExtensionsCombobox(exportImageDefaultExtension(), m_pref.exportFile.imageDefaultExtension());
fillExtensionsCombobox(exportAnimationDefaultExtension(), m_pref.exportFile.animationDefaultExtension());
fillExtensionsCombobox(exportSpriteSheetDefaultExtension(), m_pref.spriteSheet.defaultExtension());
// Number of recent items
recentFiles()->setValue(m_pref.general.recentItems());
// Color profiles
updateColorProfileControls(m_pref.color.manage(),
m_pref.color.windowProfile(),
m_pref.color.windowProfileName(),
m_pref.color.workingRgbSpace(),
m_pref.color.filesWithProfile(),
m_pref.color.missingProfile());
// Alerts
openSequence()->setSelectedItemIndex(int(m_pref.openFile.openSequence()));
// Cursor
paintingCursorType()->setSelectedItemIndex(int(m_pref.cursor.paintingCursorType()));
cursorColor()->setColor(m_pref.cursor.cursorColor());
if (cursorColor()->getColor().getType() == app::Color::MaskType) {
cursorColorType()->setSelectedItemIndex(0);
cursorColor()->setVisible(false);
}
else {
cursorColorType()->setSelectedItemIndex(1);
cursorColor()->setVisible(true);
}
// Brush preview
brushPreview()->setSelectedItemIndex(
(int)m_pref.cursor.brushPreview());
// Guide colors
layerEdgesColor()->setColor(m_pref.guides.layerEdgesColor());
autoGuidesColor()->setColor(m_pref.guides.autoGuidesColor());
// Slices default color
defaultSliceColor()->setColor(m_pref.slices.defaultColor());
// Timeline
firstFrame()->setTextf("%d", m_globPref.timeline.firstFrame());
// Others
expandMenubarOnMouseover()->setSelected(m_pref.general.expandMenubarOnMouseover());
enableDataRecovery()->setSelected(m_pref.general.dataRecovery());
if (m_pref.general.dataRecovery() &&
m_pref.general.keepEditedSpriteData())
keepEditedSpriteData()->setSelected(true);
else if (!m_pref.general.dataRecovery()) {
keepEditedSpriteData()->setEnabled(false);
keepEditedSpriteDataFor()->setEnabled(false);
}
keepClosedSpriteOnMemory()->setSelected(m_pref.general.keepClosedSpriteOnMemory());
showFullPath()->setSelected(m_pref.general.showFullPath());
dataRecoveryPeriod()->setSelectedItemIndex(
dataRecoveryPeriod()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.dataRecoveryPeriod())));
keepEditedSpriteDataFor()->setSelectedItemIndex(
keepEditedSpriteDataFor()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.keepEditedSpriteDataFor())));
keepClosedSpriteOnMemoryFor()->setSelectedItemIndex(
keepClosedSpriteOnMemoryFor()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.keepClosedSpriteOnMemoryFor())));
zoomFromCenterWithWheel()->setSelected(m_pref.editor.zoomFromCenterWithWheel());
zoomFromCenterWithKeys()->setSelected(m_pref.editor.zoomFromCenterWithKeys());
autoOpaque()->setSelected(m_pref.selection.autoOpaque());
keepSelectionAfterClear()->setSelected(m_pref.selection.keepSelectionAfterClear());
autoShowSelectionEdges()->setSelected( m_pref.selection.autoShowSelectionEdges());
moveEdges()->setSelected(m_pref.selection.moveEdges());
modifiersDisableHandles()->setSelected(m_pref.selection.modifiersDisableHandles());
moveOnAddMode()->setSelected(m_pref.selection.moveOnAddMode());
// If the platform supports native cursors...
if ((int(m_system->capabilities()) &
int(os::Capabilities::CustomMouseCursor)) != 0) {
nativeCursor()->setSelected(m_pref.cursor.useNativeCursor());
cursorScale()->setSelectedItemIndex(
cursorScale()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.cursor.cursorScale())));
}
else {
nativeCursor()->setEnabled(false);
}
onNativeCursorChange();
// "Show Aseprite file dialog" option is the inverse of the old
// experimental "use native file dialog" option
showAsepriteFileDialog()->setSelected(
!m_pref.experimental.useNativeFileDialog());
#ifdef LAF_WINDOWS // Show Tablet section on Windows
{
const os::TabletAPI tabletAPI = m_system->tabletOptions().api;
if (tabletAPI == os::TabletAPI::Wintab)
tabletApiWintabSystem()->setSelected(true);
else if (tabletAPI == os::TabletAPI::WintabPackets)
tabletApiWintabDirect()->setSelected(true);
else
tabletApiWindowsPointer()->setSelected(true);
onTabletAPIChange();
}
#endif
flashLayer()->setSelected(m_pref.experimental.flashLayer());
nonactiveLayersOpacity()->setValue(m_pref.experimental.nonactiveLayersOpacity());
m_rgbmapAlgorithmSelector.algorithm(m_pref.quantization.rgbmapAlgorithm());
m_bestFitCriteriaSelector.criteria(m_pref.quantization.fitCriteria());
showScrollbars()->setSelected(m_pref.editor.showScrollbars());
autoScroll()->setSelected(m_pref.editor.autoScroll());
straightLinePreview()->setSelected(m_pref.editor.straightLinePreview());
discardBrush()->setSelected(m_pref.eyedropper.discardBrush());
// Update the one/multiple window buttonset (and keep in on sync
// with the old/experimental checkbox)
uiWindows()->setSelectedItem(multipleWindows()->isSelected() ? 1 : 0);
// Scaling
selectScalingItems();
if (m_system->hasCapability(os::Capabilities::GpuAccelerationSwitch)) {
gpuAcceleration()->setSelected(m_pref.general.gpuAcceleration());
}
if (m_system->menus())
showMenuBar()->setSelected(m_pref.general.showMenuBar());
showHome()->setSelected(m_pref.general.showHome());
// Right-click
rightClickBehavior()->setSelectedItemIndex((int)m_pref.editor.rightClickMode());
// Undo preferences
limitUndo()->setSelected(m_pref.undo.sizeLimit() != 0);
onLimitUndoCheck();
undoGotoModified()->setSelected(m_pref.undo.gotoModified());
undoAllowNonlinearHistory()->setSelected(m_pref.undo.allowNonlinearHistory());
onChangeBgScope();
onChangeGridScope();
sectionListbox()->selectIndex(m_curSection);
}
bool ok() {
return (closer() == buttonOk());
}
void saveConfig() {
void saveConfig(bool propagate = true) {
// Save preferences in widgets that are bound to options automatically
{
if (propagate) {
Message msg(kSavePreferencesMessage);
msg.setPropagateToChildren(true);
sendMessage(&msg);
@ -756,7 +778,7 @@ public:
int j = 2;
for (auto& cs : m_colorSpaces) {
// We add ICC profiles only
auto gfxCs = cs->gfxColorSpace();
auto& gfxCs = cs->gfxColorSpace();
if (gfxCs->type() != gfx::ColorSpace::ICC)
continue;
@ -828,6 +850,7 @@ public:
m_pref.experimental.flashLayer(flashLayer()->isSelected());
m_pref.experimental.nonactiveLayersOpacity(nonactiveLayersOpacity()->getValue());
m_pref.quantization.rgbmapAlgorithm(m_rgbmapAlgorithmSelector.algorithm());
m_pref.quantization.fitCriteria(m_bestFitCriteriaSelector.criteria());
#ifdef LAF_WINDOWS
{
@ -893,9 +916,7 @@ public:
m_pref.general.showMenuBar(showMenuBar()->isSelected());
}
bool newShowHome = showHome()->isSelected();
if (newShowHome != m_pref.general.showHome())
m_pref.general.showHome(newShowHome);
m_pref.general.showHome(showHome()->isSelected());
m_pref.save();
@ -904,8 +925,7 @@ public:
}
// Probably it's safe to switch this flag in runtime
if (m_pref.experimental.multipleWindows() != ui::get_multiple_displays())
ui::set_multiple_displays(m_pref.experimental.multipleWindows());
ui::set_multiple_displays(m_pref.experimental.multipleWindows());
if (reset_screen)
updateScreenScaling();
@ -928,6 +948,15 @@ public:
}
}
void restoreDefaultTheme() {
setUITheme(m_pref.theme.selected.defaultValue(), false);
m_pref.general.screenScale.setValue(
skin::SkinTheme::get(this)->preferredScreenScaling());
m_pref.general.uiScale.setValue(
skin::SkinTheme::get(this)->preferredUIScaling());
updateScreenScaling();
}
bool showDialogToInstallExtension(const std::string& filename) {
for (Widget* item : sectionListbox()->children()) {
if (auto listItem = dynamic_cast<const ListItem*>(item)) {
@ -1037,6 +1066,133 @@ private:
m_restoreUIScaling = m_pref.general.uiScale();
}
void onResetDefault() {
if (ui::Alert::show(Strings::alerts_reset_default_confirm()) != 1)
return;
if (recentReset()->isSelected()) {
auto prevLimit = m_pref.general.recentItems();
App::instance()->recentFiles()->setLimit(0);
App::instance()->recentFiles()->setLimit(prevLimit);
}
if (installedReset()->isSelected()) {
// If we're not on the default theme, restore it, since we're gonna be deleting it.
restoreDefaultTheme();
// Load a list with the extensions we can uninstall first, to avoid iterator issues when deleting in-loop.
Extensions::List uninstall;
for (auto* e : App::instance()->extensions()) {
if (!e->canBeUninstalled())
continue;
uninstall.push_back(e);
}
for (auto* e : uninstall) {
try {
App::instance()->extensions().uninstallExtension(
e, DeletePluginPref::kYes);
}
catch (const std::exception& ex) {
LOG(ERROR, "Uninstalling extension '%s' failed with error '%s'\n",
e->displayName().c_str(),
ex.what());
Console::showException(ex);
}
}
ResourceFinder rf;
rf.includeUserDir("palettes");
const auto& paletteDir = rf.defaultFilename();
for (const auto& item : base::list_files(paletteDir)) {
const auto path = base::join_path(paletteDir, item);
if (base::is_file(path) &&
item != "default.ase" &&
base::string_to_lower(base::get_file_extension(path)) == "ase") {
try {
base::delete_file(path);
LOG(VERBOSE, "Deleted palette: '%s'\n", item.c_str());
}
catch (const std::exception& ex) {
LOG(ERROR,
"Error deleting palette file: %s - %s",
path.c_str(),
ex.what());
}
}
}
}
if (perfileReset()->isSelected()) {
ResourceFinder rf;
rf.includeUserDir("files");
const auto& filesDirectory = rf.defaultFilename();
for (const auto& item : base::list_files(filesDirectory)) {
const auto path = base::join_path(filesDirectory, item);
if (base::is_file(path) && base::string_to_lower(base::get_file_extension(path)) == "ini") {
try {
base::delete_file(path);
LOG(VERBOSE, "Deleted per-file setting '%s'\n", item.c_str());
}
catch (const std::exception& ex) {
LOG(ERROR,
"Error deleting ini file: %s - %s",
path.c_str(),
ex.what());
}
}
}
}
if (toolsReset()->isSelected()) {
auto* toolBox = App::instance()->toolBox();
for (tools::ToolIterator it = toolBox->begin(); it != toolBox->end(); ++it) {
tools::Tool* tool = *it;
m_pref.resetToolPreferences(tool);
LOG(VERBOSE, "Reset tool preferences for tool '%s'\n", tool->getId().c_str());
}
}
if (defaultReset()->isSelected()) {
onResetAlerts();
onResetBg();
onResetColorManagement();
onResetGrid();
onResetTimelineSel();
// If we're not on the default theme, restore it.
m_restoreThisTheme = m_pref.theme.selected.defaultValue();
restoreTheme();
// Resetting all things.
for (Section* section : m_pref.sectionList()) {
for (OptionBase* option : section->optionList()) {
option->resetToDefault();
}
section->save();
}
restoreDefaultTheme();
m_pref.save();
loadFromPreferences();
// Temporarily set the language preference to an empty string
// to avoid setCurrentLanguage ignoring the change.
m_pref.general.language("");
Strings::instance()->setCurrentLanguage(Strings::kDefLanguage);
// Language reset
refillLanguages();
}
saveConfig(false);
closeWindow(nullptr);
}
void onNativeCursorChange() {
bool state =
// If the platform supports custom cursors...
@ -1126,11 +1282,11 @@ private:
int j = 2;
for (auto& cs : m_colorSpaces) {
// We add ICC profiles only
auto gfxCs = cs->gfxColorSpace();
auto& gfxCs = cs->gfxColorSpace();
if (gfxCs->type() != gfx::ColorSpace::ICC)
continue;
auto name = gfxCs->name();
auto& name = gfxCs->name();
windowCs()->addItem(fmt::format(m_templateTextForDisplayCS, name));
if (windowProfile == gen::WindowColorProfile::SPECIFIC &&
windowProfileName == name) {
@ -1179,6 +1335,7 @@ private:
jpegOptionsAlert()->resetWithDefaultValue();
svgOptionsAlert()->resetWithDefaultValue();
tgaOptionsAlert()->resetWithDefaultValue();
webpOptionsAlert()->resetWithDefaultValue();
}
void onChangeBgScope() {
@ -1842,6 +1999,7 @@ private:
std::vector<os::ColorSpaceRef> m_colorSpaces;
std::string m_templateTextForDisplayCS;
RgbMapAlgorithmSelector m_rgbmapAlgorithmSelector;
BestFitCriteriaSelector m_bestFitCriteriaSelector;
ButtonSet* m_themeVars = nullptr;
SamplingSelector* m_samplingSelector = nullptr;
};

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -10,8 +11,11 @@
#include "app/app.h"
#include "app/commands/command.h"
#include "app/commands/params.h"
#include "app/ui/input_chain.h"
#include <memory>
namespace app {
class PasteCommand : public Command {
@ -19,8 +23,11 @@ public:
PasteCommand();
protected:
void onLoadParams(const Params& params) override;
bool onEnabled(Context* ctx) override;
void onExecute(Context* ctx) override;
private:
std::unique_ptr<gfx::Point> m_position;
};
PasteCommand::PasteCommand()
@ -28,6 +35,16 @@ PasteCommand::PasteCommand()
{
}
void PasteCommand::onLoadParams(const Params& params)
{
m_position.reset();
if (params.has_param("x") || params.has_param("y")) {
m_position.reset(new gfx::Point);
m_position->x = params.get_as<int>("x");
m_position->y = params.get_as<int>("y");
}
}
bool PasteCommand::onEnabled(Context* ctx)
{
return App::instance()->inputChain().canPaste(ctx);
@ -35,7 +52,7 @@ bool PasteCommand::onEnabled(Context* ctx)
void PasteCommand::onExecute(Context* ctx)
{
App::instance()->inputChain().paste(ctx);
App::instance()->inputChain().paste(ctx, m_position.get());
}
Command* CommandFactory::createPasteCommand()

View File

@ -39,6 +39,7 @@ protected:
void onLoadParams(const Params& params) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
private:
std::string m_filename;

View File

@ -34,6 +34,7 @@ public:
protected:
void onLoadParams(const Params& params) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
private:
std::string m_preset;
@ -101,6 +102,15 @@ void SavePaletteCommand::onExecute(Context* ctx)
}
}
std::string SavePaletteCommand::onGetFriendlyName() const
{
if (m_preset == "default")
return Strings::commands_SavePaletteAsDefault();
else if (m_saveAsPreset)
return Strings::commands_SavePaletteAsPreset();
return Command::onGetFriendlyName();
}
Command* CommandFactory::createSavePaletteCommand()
{
return new SavePaletteCommand;

View File

@ -45,6 +45,7 @@ protected:
void onLoadParams(const Params& params) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
private:
void selectTiles(const Layer* layer,

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (c) 2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -23,6 +24,8 @@ protected:
void onLoadParams(const Params& params) override;
bool onChecked(Context* context) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
private:
int m_size;
@ -50,6 +53,11 @@ void SetPaletteEntrySizeCommand::onExecute(Context* context)
ColorBar::instance()->getPaletteView()->setBoxSize(m_size);
}
std::string SetPaletteEntrySizeCommand::onGetFriendlyName() const
{
return Command::onGetFriendlyName() + " " + std::to_string(m_size);
}
Command* CommandFactory::createSetPaletteEntrySizeCommand()
{
return new SetPaletteEntrySizeCommand;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -12,6 +13,7 @@
#include "app/commands/command.h"
#include "app/commands/params.h"
#include "app/context.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "filters/tiled_mode.h"
@ -26,6 +28,8 @@ protected:
bool onEnabled(Context* context) override;
bool onChecked(Context* context) override;
void onExecute(Context* context) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
filters::TiledMode m_mode;
};
@ -64,6 +68,18 @@ void TiledModeCommand::onExecute(Context* ctx)
Preferences::instance().document(doc).tiled.mode(m_mode);
}
std::string TiledModeCommand::onGetFriendlyName() const
{
std::string mode;
switch (m_mode) {
case filters::TiledMode::NONE: mode = Strings::commands_TiledMode_None(); break;
case filters::TiledMode::BOTH: mode = Strings::commands_TiledMode_Both(); break;
case filters::TiledMode::X_AXIS: mode = Strings::commands_TiledMode_X(); break;
case filters::TiledMode::Y_AXIS: mode = Strings::commands_TiledMode_Y(); break;
}
return Strings::commands_TiledMode(mode);
}
Command* CommandFactory::createTiledModeCommand()
{
return new TiledModeCommand;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2019-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -30,10 +30,12 @@ protected:
void onExecute(Context* context) override {
auto colorBar = ColorBar::instance();
colorBar->setTilemapMode(
colorBar->tilemapMode() == TilemapMode::Pixels ?
TilemapMode::Tiles:
TilemapMode::Pixels);
if (!colorBar->isTilemapModeLocked()) {
colorBar->setTilemapMode(
colorBar->tilemapMode() == TilemapMode::Pixels ?
TilemapMode::Tiles:
TilemapMode::Pixels);
}
}
};

View File

@ -11,6 +11,7 @@
#include "app/commands/command_factory.h"
#include "app/commands/command_ids.h"
#include "app/ui/key_context.h"
#include <string>
@ -37,6 +38,10 @@ namespace app {
bool isEnabled(Context* context);
bool isChecked(Context* context);
// Returns true if the command must be displayed in the Keyboard
// Shortcuts list.
virtual bool isListed(const Params& params) const { return true; }
protected:
virtual bool onNeedsParams() const;
virtual void onLoadParams(const Params& params);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -54,6 +54,7 @@ public:
protected:
void onExecute(Context* ctx) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
};
ScreenshotCommand::ScreenshotCommand()

View File

@ -26,6 +26,7 @@ protected:
bool onChecked(Context* ctx) override;
void onExecute(Context* ctx) override;
std::string onGetFriendlyName() const override;
bool isListed(const Params& params) const override { return !params.empty(); }
};
SetPlaybackSpeedCommand::SetPlaybackSpeedCommand()

View File

@ -54,6 +54,8 @@ protected:
return Strings::commands_TilesetMode(mode);
}
bool isListed(const Params& params) const override { return !params.empty(); }
private:
TilesetMode m_mode;
};

View File

@ -143,29 +143,27 @@ void DataRecovery::searchForSessions()
// Existent sessions
RECO_TRACE("RECO: Listing sessions from '%s'\n", m_sessionsDir.c_str());
for (auto& itemname : base::list_files(m_sessionsDir)) {
std::string itempath = base::join_path(m_sessionsDir, itemname);
if (base::is_directory(itempath)) {
RECO_TRACE("RECO: Session '%s'\n", itempath.c_str());
for (const auto& itemname : base::list_files(m_sessionsDir, base::ItemType::Directories)) {
const auto& itempath = base::join_path(m_sessionsDir, itemname);
RECO_TRACE("RECO: Session '%s' ", itempath.c_str());
SessionPtr session(new Session(&m_config, itempath));
if (!session->isRunning()) {
if ((session->isEmpty()) ||
(!session->isCrashedSession() && session->isOldSession())) {
RECO_TRACE("RECO: - to be deleted (%s)\n",
session->isEmpty() ? "is empty":
(session->isOldSession() ? "is old":
"unknown reason"));
session->removeFromDisk();
}
else {
RECO_TRACE("RECO: - to be loaded\n");
sessions.push_back(session);
}
SessionPtr session(new Session(&m_config, itempath));
if (!session->isRunning()) {
if ((session->isEmpty()) ||
(!session->isCrashedSession() && session->isOldSession())) {
RECO_TRACE("to be deleted (%s)\n",
session->isEmpty() ? "is empty":
(session->isOldSession() ? "is old":
"unknown reason"));
session->removeFromDisk();
}
else {
RECO_TRACE("to be loaded\n");
sessions.push_back(session);
}
else
RECO_TRACE("is running\n");
}
else
RECO_TRACE("is running\n");
}
// Sort sessions from the most recent one to the oldest one

View File

@ -77,7 +77,7 @@ public:
, m_docVersions(nullptr)
, m_loadInfo(nullptr)
, m_taskToken(t) {
for (const auto& fn : base::list_files(dir)) {
for (const auto& fn : base::list_files(dir, base::ItemType::Files)) {
auto i = fn.find('-');
if (i == std::string::npos)
continue; // Has no ID
@ -91,7 +91,12 @@ public:
if (!id || !ver)
continue; // Error converting strings to ID/ver
if (!check_magic_number(base::join_path(m_dir, fn))) {
// Checking for the magic number of each file takes a long time,
// we can guess that all files are valid when there is no
// m_taskToken, i.e. when we have to just show the description
// of the doc in the list of backups.
if (m_taskToken &&
!check_magic_number(base::join_path(m_dir, fn))) {
RECO_TRACE("RECO: Ignoring invalid file %s (no magic number)\n", fn.c_str());
continue;
}

View File

@ -30,6 +30,7 @@
#include "base/thread.h"
#include "base/time.h"
#include "doc/cancel_io.h"
#include "ui/app_state.h"
#include "fmt/format.h"
#include "ver/info.h"
@ -43,22 +44,24 @@ static const char* kOpenFilename = "open"; // File that indicates if the documen
Session::Backup::Backup(const std::string& dir)
: m_dir(dir)
{
DocumentInfo info;
read_document_info(dir, info);
m_fn = info.filename;
m_desc =
fmt::format("{} Sprite {}x{}, {} {}",
info.mode == ColorMode::RGB ? "RGB":
info.mode == ColorMode::GRAYSCALE ? "Grayscale":
info.mode == ColorMode::INDEXED ? "Indexed":
info.mode == ColorMode::BITMAP ? "Bitmap": "Unknown",
info.width, info.height, info.frames,
info.frames == 1 ? "frame": "frames");
}
std::string Session::Backup::description(const bool withFullPath) const
{
// Lazy initialize description and filename.
if (m_desc.empty()) {
DocumentInfo info;
read_document_info(m_dir, info);
m_fn = info.filename;
m_desc =
fmt::format("{} Sprite {}x{}, {} {}",
info.mode == ColorMode::RGB ? "RGB":
info.mode == ColorMode::GRAYSCALE ? "Grayscale":
info.mode == ColorMode::INDEXED ? "Indexed":
info.mode == ColorMode::BITMAP ? "Bitmap": "Unknown",
info.width, info.height, info.frames,
info.frames == 1 ? "frame": "frames");
}
return fmt::format("{}: {}",
m_desc,
withFullPath ? m_fn:
@ -114,11 +117,12 @@ std::string Session::version()
const Session::Backups& Session::backups()
{
if (m_backups.empty()) {
for (auto& item : base::list_files(m_path)) {
for (const auto& item : base::list_files(m_path, base::ItemType::Directories)) {
if (ui::is_app_state_closing())
continue;
std::string docDir = base::join_path(m_path, item);
if (base::is_directory(docDir)) {
m_backups.push_back(std::make_shared<Backup>(docDir));
}
m_backups.push_back(std::make_shared<Backup>(docDir));
}
}
return m_backups;
@ -147,18 +151,40 @@ bool Session::isOldSession()
return true;
int lifespanDays = m_config->keepEditedSpriteDataFor;
base::Time sessionTime = base::get_modification_time(verfile);
base::Time sessionTime;
// Get the session time from the name if possible, to avoid re-scanning when transferring files
std::vector<std::string> parts;
base::split_string(base::get_file_title(m_path), parts, "-");
if (parts.size() == 3 && parts[0].size() == 8 && parts[1].size() == 6) {
try {
sessionTime = base::Time(filenamePartToInt(parts[0].substr(0, 4)),
filenamePartToInt(parts[0].substr(4, 2)),
filenamePartToInt(parts[0].substr(6, 2)),
filenamePartToInt(parts[1].substr(0, 2)),
filenamePartToInt(parts[1].substr(2, 2)),
filenamePartToInt(parts[1].substr(4, 2)));
}
catch (const std::exception& ex) {
LOG(ERROR,
"Failed to parse a date from '%s', error: %s",
parts[0].c_str(),
ex.what());
}
}
if (!sessionTime.valid()) {
// Get modification time as a fallback
sessionTime = base::get_modification_time(verfile);
}
return (sessionTime.addDays(lifespanDays) < base::current_time());
}
bool Session::isEmpty()
{
for (auto& item : base::list_files(m_path)) {
if (base::is_directory(base::join_path(m_path, item)))
return false;
}
return true;
return base::list_files(m_path, base::ItemType::Directories).empty();
}
void Session::create(base::pid pid)
@ -385,12 +411,13 @@ void Session::deleteDirectory(const std::string& dir)
if (dir.empty())
return;
for (auto& item : base::list_files(dir)) {
for (const auto& item : base::list_files(dir, base::ItemType::Files)) {
if (ui::is_app_state_closing())
return;
std::string objfn = base::join_path(dir, item);
if (base::is_file(objfn)) {
RECO_TRACE("RECO: Deleting file '%s'\n", objfn.c_str());
base::delete_file(objfn);
}
RECO_TRACE("RECO: Deleting file '%s'\n", objfn.c_str());
base::delete_file(objfn);
}
base::remove_directory(dir);
}
@ -411,5 +438,20 @@ void Session::fixFilename(Doc* doc)
base::get_file_title(fn) + "-Recovered" + ext));
}
int Session::filenamePartToInt(const std::string& part) const
{
if (part.empty())
throw base::Exception("Invalid part");
int result = std::strtol(part.c_str(), NULL, 10);
if (errno == ERANGE)
throw base::Exception("Number out of range");
if (result < 0)
throw base::Exception("Negative value");
return result;
}
} // namespace crash
} // namespace app

View File

@ -35,8 +35,8 @@ namespace crash {
std::string description(const bool withFullPath) const;
private:
std::string m_dir;
std::string m_desc;
std::string m_fn;
mutable std::string m_desc;
mutable std::string m_fn;
};
using BackupPtr = std::shared_ptr<Backup>;
using Backups = std::vector<BackupPtr>;
@ -78,6 +78,7 @@ namespace crash {
void markDocumentAsCorrectlyClosed(Doc* doc);
void deleteDirectory(const std::string& dir);
void fixFilename(Doc* doc);
int filenamePartToInt(const std::string& part) const;
base::pid m_pid;
std::string m_path;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -1289,13 +1289,18 @@ void DocExporter::renderTexture(Context* ctx,
// Make the sprite compatible with the texture so the render()
// works correctly.
if (sample.sprite()->pixelFormat() != textureImage->pixelFormat()) {
RgbMapAlgorithm rgbmapAlgo =
Preferences::instance().quantization.rgbmapAlgorithm();
FitCriteria fc =
Preferences::instance().quantization.fitCriteria();
cmd::SetPixelFormat(
sample.sprite(),
textureImage->pixelFormat(),
render::Dithering(),
Sprite::DefaultRgbMapAlgorithm(), // TODO add rgbmap algorithm preference
rgbmapAlgo,
nullptr, // toGray is not needed because the texture is Indexed or RGB
nullptr) // TODO add a delegate to show progress
nullptr, // TODO add a delegate to show progress
fc)
.execute(ctx);
}

View File

@ -794,7 +794,7 @@ Extensions::Extensions()
// Create and get the user extensions directory
{
ResourceFinder rf2;
rf2.includeUserDir("extensions/.");
rf2.includeUserDir("extensions");
m_userExtensionsPath = rf2.getFirstOrCreateDefault();
m_userExtensionsPath = base::normalize_path(m_userExtensionsPath);
if (!m_userExtensionsPath.empty() &&
@ -811,33 +811,31 @@ Extensions::Extensions()
// Load extensions from data/ directory on all possible locations
// (installed folder and user folder)
while (rf.next()) {
auto extensionsDir = rf.filename();
const auto& extensionsDir = rf.filename();
if (base::is_directory(extensionsDir)) {
for (auto fn : base::list_files(extensionsDir)) {
const auto dir = base::join_path(extensionsDir, fn);
if (!base::is_directory(dir))
continue;
if (!base::is_directory(extensionsDir))
continue;
const bool isBuiltinExtension =
(m_userExtensionsPath != base::get_file_path(dir));
for (const auto& fn : base::list_files(extensionsDir, base::ItemType::Directories)) {
const auto dir = base::join_path(extensionsDir, fn);
const bool isBuiltinExtension =
(m_userExtensionsPath != base::get_file_path(dir));
auto fullFn = base::join_path(dir, kPackageJson);
fullFn = base::normalize_path(fullFn);
auto fullFn = base::join_path(dir, kPackageJson);
fullFn = base::normalize_path(fullFn);
LOG("EXT: Loading extension '%s'...\n", fullFn.c_str());
if (!base::is_file(fullFn)) {
LOG("EXT: File '%s' not found\n", fullFn.c_str());
continue;
}
LOG("EXT: Loading extension '%s'...\n", fullFn.c_str());
if (!base::is_file(fullFn)) {
LOG("EXT: File '%s' not found\n", fullFn.c_str());
continue;
}
try {
loadExtension(dir, fullFn, isBuiltinExtension);
}
catch (const std::exception& ex) {
LOG("EXT: Error loading JSON file: %s\n",
ex.what());
}
try {
loadExtension(dir, fullFn, isBuiltinExtension);
}
catch (const std::exception& ex) {
LOG("EXT: Error loading JSON file: %s\n",
ex.what());
}
}
}

View File

@ -295,6 +295,13 @@ bool AseFormat::onLoad(FileOp* fop)
return false;
Sprite* sprite = delegate.sprite();
// Assign RgbMap
if (sprite->pixelFormat() == IMAGE_INDEXED)
sprite->rgbMap(0, Sprite::RgbMapFor(sprite->isOpaque()),
fop->config().rgbMapAlgorithm,
fop->config().fitCriteria);
fop->createDocument(sprite);
if (sprite->colorSpace() != nullptr &&

View File

@ -1356,6 +1356,11 @@ ImageRef FileOp::sequenceImageToLoad(
// Add the layer
sprite->root()->addLayer(layer);
// Assign RgbMap
if (sprite->pixelFormat() == IMAGE_INDEXED)
sprite->rgbMap(0, Sprite::RgbMapFor(sprite->isOpaque()),
m_config.rgbMapAlgorithm,
m_config.fitCriteria);
// Done
createDocument(sprite);
m_seq.layer = layer;

View File

@ -25,6 +25,7 @@ void FileOpConfig::fillFromPreferences()
defaultSliceColor = pref.slices.defaultColor();
workingCS = get_working_rgb_space_from_preferences();
rgbMapAlgorithm = pref.quantization.rgbmapAlgorithm();
fitCriteria = pref.quantization.fitCriteria();
cacheCompressedTilesets = pref.tileset.cacheCompressedTilesets();
composeGroups = pref.experimental.composeGroups();
}

View File

@ -33,9 +33,14 @@ namespace app {
app::Color defaultSliceColor = app::Color::fromRgb(0, 0, 255);
// Algorithm used to create a palette from RGB files.
// Algorithm used to fit any color into the available palette colors in
// Indexed Color Mode.
doc::RgbMapAlgorithm rgbMapAlgorithm = doc::RgbMapAlgorithm::DEFAULT;
// Fit criteria used to compare colors during the conversion to
// Indexed Color Mode.
doc::FitCriteria fitCriteria = doc::FitCriteria::DEFAULT;
// Cache compressed tilesets. When we load a tileset from a
// .aseprite file, the compressed data will be stored on memory to
// make the save operation faster (as we can re-use the already

View File

@ -432,6 +432,7 @@ FormatOptionsPtr WebPFormat::onAskUserForFormatOptions(FileOp* fop)
pref.webp.imageHint(base::convert_to<int>(win.imageHint()->getValue()));
pref.webp.quality(win.quality()->getValue());
pref.webp.imagePreset(base::convert_to<int>(win.imagePreset()->getValue()));
pref.webp.showAlert(!win.dontShow()->isSelected());
opts->setLoop(pref.webp.loop());
opts->setType(WebPOptions::Type(pref.webp.type()));

View File

@ -36,10 +36,9 @@ void get_font_dirs(base::paths& fontDirs)
fontDirs.push_back(fontDir);
for (const auto& file : base::list_files(fontDir)) {
for (const auto& file : base::list_files(fontDir, base::ItemType::Directories)) {
std::string fullpath = base::join_path(fontDir, file);
if (base::is_directory(fullpath))
q.push(fullpath); // Add subdirectory in the queue
q.push(fullpath); // Add subdirectory in the queue
}
}

View File

@ -64,11 +64,7 @@ std::set<LangInfo> Strings::availableLanguages() const
if (!base::is_directory(stringsPath))
continue;
for (const auto& fn : base::list_files(stringsPath)) {
// Ignore README/LICENSE files.
if (base::get_file_extension(fn) != "ini")
continue;
for (const auto& fn : base::list_files(stringsPath, base::ItemType::Files, "*.ini")) {
const std::string langId = base::get_file_title(fn);
std::string path = base::join_path(stringsPath, fn);
std::string displayName = langId;

View File

@ -22,12 +22,15 @@ namespace app {
class Section {
public:
Section(const std::string& name) : m_name(name) { }
virtual ~Section() { }
explicit Section(const std::string& name) : m_name(name) { }
virtual ~Section() = default;
const char* name() const { return m_name.c_str(); }
virtual Section* section(const char* id) = 0;
virtual OptionBase* option(const char* id) = 0;
virtual std::vector<OptionBase*> optionList() const { return {}; }
virtual std::vector<Section*> sectionList() const { return {}; }
virtual void save() = 0;
obs::signal<void()> BeforeChange;
obs::signal<void()> AfterChange;
@ -42,9 +45,10 @@ namespace app {
: m_section(section)
, m_id(id) {
}
virtual ~OptionBase() { }
virtual ~OptionBase() = default;
const char* section() const { return m_section->name(); }
const char* id() const { return m_id; }
virtual void resetToDefault() = 0;
#ifdef ENABLE_SCRIPTING
virtual void pushLua(lua_State* L) = 0;
@ -127,6 +131,10 @@ namespace app {
m_dirty = false;
}
void resetToDefault() override {
setValue(m_default);
}
#ifdef ENABLE_SCRIPTING
void pushLua(lua_State* L) override {
script::push_value_to_lua<T>(L, m_value);

View File

@ -67,14 +67,6 @@ Preferences::Preferences()
load();
// Create a connection with the default RgbMapAlgorithm preferences
// to change the default algorithm in the "doc" layer.
quantization.rgbmapAlgorithm.AfterChange.connect(
[](const doc::RgbMapAlgorithm& newValue){
doc::Sprite::SetDefaultRgbMapAlgorithm(newValue);
});
doc::Sprite::SetDefaultRgbMapAlgorithm(quantization.rgbmapAlgorithm());
// Create a connection with the default document preferences grid
// bounds to sync the default grid bounds for new sprites in the
// "doc" layer.

View File

@ -58,7 +58,7 @@ namespace app {
Preferences();
~Preferences();
void save();
void save() override;
// Returns true if the given option was set by the user or false
// if it contains the default value.

View File

@ -45,7 +45,7 @@ void PalettesLoaderDelegate::getResourcesPaths(std::map<std::string, std::string
if (base::is_directory(rf.filename())) {
path = rf.filename();
path = base::fix_path_separators(path);
for (const auto& fn : base::list_files(path)) {
for (const auto& fn : base::list_files(path, base::ItemType::Files)) {
// Ignore the default palette that is inside the palettes/ dir
// in the user home dir.
if (fn == "default.ase" ||
@ -53,8 +53,7 @@ void PalettesLoaderDelegate::getResourcesPaths(std::map<std::string, std::string
continue;
std::string fullFn = base::join_path(path, fn);
if (base::is_file(fullFn))
idAndPath[base::get_file_title(fn)] = fullFn;
idAndPath[base::get_file_title(fn)] = fullFn;
}
}
}

View File

@ -110,7 +110,7 @@ int AppFS_listFiles(lua_State* L)
lua_newtable(L);
if (path) {
int i = 0;
for (auto fn : base::list_files(path)) {
for (const auto& fn : base::list_files(path)) {
lua_pushstring(L, fn.c_str());
lua_seti(L, -2, ++i);
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2015-2018 David Capello
//
// This program is distributed under the terms of
@ -348,21 +348,37 @@ int App_useTool(lua_State* L)
params.inkType = get_value_from_lua<tools::InkType>(L, -1);
lua_pop(L, 1);
// Are we going to modify pixels or tiles?
type = lua_getfield(L, 1, "tilemapMode");
if (type != LUA_TNIL) {
site.tilemapMode(TilemapMode(lua_tointeger(L, -1)));
}
lua_pop(L, 1);
// How the tileset must be modified depending on this tool usage
type = lua_getfield(L, 1, "tilesetMode");
if (type != LUA_TNIL) {
site.tilesetMode(TilesetMode(lua_tointeger(L, -1)));
}
lua_pop(L, 1);
// Color
type = lua_getfield(L, 1, "color");
if (type != LUA_TNIL)
params.fg = convert_args_into_color(L, -1);
else {
// Default color is the active fgColor
else if (site.tilemapMode() == TilemapMode::Tiles)
params.fg = Color::fromTile(Preferences::instance().colorBar.fgTile());
else // Default color is the active fgColor
params.fg = Preferences::instance().colorBar.fgColor();
}
lua_pop(L, 1);
type = lua_getfield(L, 1, "bgColor");
if (type != LUA_TNIL)
params.bg = convert_args_into_color(L, -1);
else if (site.tilemapMode() == TilemapMode::Tiles)
params.bg = Color::fromTile(Preferences::instance().colorBar.bgTile());
else
params.bg = params.fg;
params.bg = Preferences::instance().colorBar.bgColor();
lua_pop(L, 1);
// Adjust ink depending on "inkType" and "color"
@ -441,20 +457,6 @@ int App_useTool(lua_State* L)
}
}
// Are we going to modify pixels or tiles?
type = lua_getfield(L, 1, "tilemapMode");
if (type != LUA_TNIL) {
site.tilemapMode(TilemapMode(lua_tointeger(L, -1)));
}
lua_pop(L, 1);
// How the tileset must be modified depending on this tool usage
type = lua_getfield(L, 1, "tilesetMode");
if (type != LUA_TNIL) {
site.tilesetMode(TilesetMode(lua_tointeger(L, -1)));
}
lua_pop(L, 1);
// Do the tool loop
type = lua_getfield(L, 1, "points");
if (type == LUA_TTABLE) {
@ -470,6 +472,13 @@ int App_useTool(lua_State* L)
bool first = true;
lua_pushnil(L);
tools::ToolBox* toolbox = App::instance()->toolBox();
const bool isSelectionInk =
(params.ink == toolbox->getInkById(tools::WellKnownInks::Selection));
const tools::Pointer::Button button =
(!isSelectionInk ? (buttonIdx == 0 ? tools::Pointer::Button::Left :
tools::Pointer::Button::Right) :
tools::Pointer::Button::Left);
while (lua_next(L, -2) != 0) {
gfx::Point pt = convert_args_into_point(L, -1);
@ -477,7 +486,7 @@ int App_useTool(lua_State* L)
pt,
// TODO configurable params
tools::Vec2(0.0f, 0.0f),
tools::Pointer::Button::Left,
button,
tools::Pointer::Type::Unknown,
0.0f);
if (first) {
@ -635,6 +644,30 @@ int App_set_bgColor(lua_State* L)
return 0;
}
int App_get_fgTile(lua_State* L)
{
lua_pushinteger(L, Preferences::instance().colorBar.fgTile());
return 1;
}
int App_set_fgTile(lua_State* L)
{
Preferences::instance().colorBar.fgTile(lua_tointeger(L, 2));
return 0;
}
int App_get_bgTile(lua_State* L)
{
lua_pushinteger(L, Preferences::instance().colorBar.bgTile());
return 1;
}
int App_set_bgTile(lua_State* L)
{
Preferences::instance().colorBar.bgTile(lua_tointeger(L, 2));
return 0;
}
int App_get_site(lua_State* L)
{
app::Context* ctx = App::instance()->context();
@ -819,6 +852,8 @@ const Property App_properties[] = {
{ "sprites", App_get_sprites, nullptr },
{ "fgColor", App_get_fgColor, App_set_fgColor },
{ "bgColor", App_get_bgColor, App_set_bgColor },
{ "fgTile", App_get_fgTile, App_set_fgTile },
{ "bgTile", App_get_bgTile, App_set_bgTile },
{ "version", App_get_version, nullptr },
{ "apiVersion", App_get_apiVersion, nullptr },
{ "site", App_get_site, nullptr },

View File

@ -0,0 +1,133 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/script/script_input_chain.h"
#include "app/app.h"
#include "app/cmd/deselect_mask.h"
#include "app/cmd/remap_colors.h"
#include "app/commands/commands.h"
#include "app/context_access.h"
#include "app/site.h"
#include "app/tx.h"
#include "app/util/clipboard.h"
#include "doc/mask.h"
#include "doc/layer.h"
#include "doc/primitives.h"
#include <cstring>
#include <limits>
#include <memory>
namespace app {
ScriptInputChain::~ScriptInputChain() { }
void ScriptInputChain::onNewInputPriority(InputChainElement* element,
const ui::Message* msg) { }
bool ScriptInputChain::onCanCut(Context* ctx)
{
return ctx->activeDocument() &&
ctx->activeDocument()->isMaskVisible();
}
bool ScriptInputChain::onCanCopy(Context* ctx)
{
return onCanCut(ctx);
}
bool ScriptInputChain::onCanPaste(Context* ctx)
{
const Clipboard* clipboard(ctx->clipboard());
if (!clipboard)
return false;
return clipboard->format() == ClipboardFormat::Image &&
ctx->activeSite().layer() &&
ctx->activeSite().layer()->type() == ObjectType::LayerImage;
}
bool ScriptInputChain::onCanClear(Context* ctx)
{
return onCanCut(ctx);
}
bool ScriptInputChain::onCut(Context* ctx)
{
ContextWriter writer(ctx);
Clipboard* clipboard = ctx->clipboard();
if (!clipboard)
return false;
if (writer.document()) {
clipboard->cut(writer);
return true;
}
return false;
}
bool ScriptInputChain::onCopy(Context* ctx)
{
ContextReader reader(ctx);
Clipboard* clipboard = ctx->clipboard();
if (!clipboard)
return false;
if (reader.document()) {
clipboard->copy(reader);
return true;
}
return false;
}
bool ScriptInputChain::onPaste(Context* ctx,
const gfx::Point* position)
{
Clipboard* clipboard = ctx->clipboard();
if (!clipboard)
return false;
if (clipboard->format() == ClipboardFormat::Image) {
clipboard->paste(ctx, false, position);
return true;
}
return false;
}
bool ScriptInputChain::onClear(Context* ctx)
{
// TODO This code is similar to DocView::onClear() and Clipboard::cut()
ContextWriter writer(ctx);
Doc* document = ctx->activeDocument();
if (writer.document()) {
ctx->clipboard()->clearContent();
CelList cels;
const Site site = ctx->activeSite();
cels.push_back(site.cel());
if (cels.empty()) // No cels to modify
return false;
Tx tx(writer, "Clear");
ctx->clipboard()->clearMaskFromCels(
tx, document, site, cels, true);
tx.commit();
return true;
}
return false;
}
void ScriptInputChain::onCancel(Context* ctx)
{
// Deselect mask
if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasVisibleMask)) {
Command* deselectMask = Commands::instance()->byId(CommandId::DeselectMask());
ctx->executeCommand(deselectMask);
ctx->activeDocument()->setMaskVisible(false);
}
}
} // namespace app

View File

@ -0,0 +1,40 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_SCRIPT_SCRIPT_INPUT_CHAIN_H_INCLUDED
#define APP_SCRIPT_SCRIPT_INPUT_CHAIN_H_INCLUDED
#pragma once
#ifndef ENABLE_SCRIPTING
#error ENABLE_SCRIPTING must be defined
#endif
#include "app/ui/input_chain_element.h"
namespace app {
class ScriptInputChain : public InputChainElement {
public:
// InputChainElement impl
~ScriptInputChain() override;
void onNewInputPriority(InputChainElement* element,
const ui::Message* msg) override;
bool onCanCut(Context* ctx) override;
bool onCanCopy(Context* ctx) override;
bool onCanPaste(Context* ctx) override;
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;
};
} // namespace app
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -83,6 +83,26 @@ int Site_get_image(lua_State* L)
return 1;
}
int Site_get_tilemapMode(lua_State* L)
{
auto site = get_obj<Site>(L, 1);
if (site)
lua_pushinteger(L, (int)site->tilemapMode());
else
lua_pushnil(L);
return 1;
}
int Site_get_tilesetMode(lua_State* L)
{
auto site = get_obj<Site>(L, 1);
if (site)
lua_pushinteger(L, (int)site->tilesetMode());
else
lua_pushnil(L);
return 1;
}
const luaL_Reg Site_methods[] = {
{ nullptr, nullptr }
};
@ -94,6 +114,8 @@ const Property Site_properties[] = {
{ "frame", Site_get_frame, nullptr },
{ "frameNumber", Site_get_frameNumber, nullptr },
{ "image", Site_get_image, nullptr },
{ "tilemapMode", Site_get_tilemapMode, nullptr },
{ "tilesetMode", Site_get_tilesetMode, nullptr },
{ nullptr, nullptr, nullptr }
};

View File

@ -339,7 +339,9 @@ int Sprite_flatten(lua_State* L)
range.selectLayer(layer);
Tx tx(sprite);
tx(new cmd::FlattenLayers(sprite, range.selectedLayers(), true));
cmd::FlattenLayers::Options options;
options.newBlendMethod = true;
tx(new cmd::FlattenLayers(sprite, range.selectedLayers(), options));
tx.commit();
return 0;
}

View File

@ -342,6 +342,7 @@ FOR_ENUM(app::tools::RotationAlgorithm)
FOR_ENUM(doc::AniDir)
FOR_ENUM(doc::BrushPattern)
FOR_ENUM(doc::ColorMode)
FOR_ENUM(doc::FitCriteria)
FOR_ENUM(doc::RgbMapAlgorithm)
FOR_ENUM(filters::HueSaturationFilter::Mode)
FOR_ENUM(filters::TiledMode)

View File

@ -109,18 +109,15 @@ bool Sentry::areThereCrashesToReport()
// At least one .dmp file in the completed/ directory means that
// there was at least one crash in the past (this is for macOS).
for (auto f : base::list_files(base::join_path(m_dbdir, "completed"))) {
if (base::get_file_extension(f) == "dmp")
return true;
}
if (!base::join_path(m_dbdir, "completed"), base::ItemType::Files, "*.dmp").empty())
return true;
// In case that "last_crash" doesn't exist we can check for some
// .dmp file in the reports/ directory (it looks like the completed/
// directory is not generated on Windows).
for (auto f : base::list_files(base::join_path(m_dbdir, "reports"))) {
if (base::get_file_extension(f) == "dmp")
return true;
}
if (!base::list_files(base::join_path(m_dbdir, "reports"), base::ItemType::Files, "*.dmp").empty())
return true;
return false;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -570,6 +570,12 @@ public:
ReplaceInkProcessing(ToolLoop* loop) {
m_color1 = loop->getPrimaryColor();
m_color2 = loop->getSecondaryColor();
if (loop->getLayer()->isBackground()) {
switch (loop->sprite()->pixelFormat()) {
case IMAGE_RGB: m_color2 |= rgba_a_mask; break;
case IMAGE_GRAYSCALE: m_color2 |= graya_a_mask; break;
}
}
m_opacity = loop->getOpacity();
}

View File

@ -0,0 +1,46 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/best_fit_criteria_selector.h"
#include "app/i18n/strings.h"
namespace app {
BestFitCriteriaSelector::BestFitCriteriaSelector()
{
// addItem() must match the FitCriteria enum
static_assert(int(doc::FitCriteria::DEFAULT) == 0 &&
int(doc::FitCriteria::RGB) == 1 &&
int(doc::FitCriteria::linearizedRGB) == 2 &&
int(doc::FitCriteria::CIEXYZ) == 3 &&
int(doc::FitCriteria::CIELAB) == 4,
"Unexpected doc::FitCriteria values");
addItem(Strings::best_fit_criteria_selector_default());
addItem(Strings::best_fit_criteria_selector_rgb());
addItem(Strings::best_fit_criteria_selector_linearized_rgb());
addItem(Strings::best_fit_criteria_selector_cie_xyz());
addItem(Strings::best_fit_criteria_selector_cie_lab());
criteria(doc::FitCriteria::DEFAULT);
}
doc::FitCriteria BestFitCriteriaSelector::criteria()
{
return (doc::FitCriteria)getSelectedItemIndex();
}
void BestFitCriteriaSelector::criteria(const doc::FitCriteria criteria)
{
setSelectedItemIndex((int)criteria);
}
} // namespace app

View File

@ -0,0 +1,27 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_BEST_FIT_CRITERIA_SELECTOR_H_INCLUDED
#define APP_UI_BEST_FIT_CRITERIA_SELECTOR_H_INCLUDED
#pragma once
#include "doc/fit_criteria.h"
#include "doc/palette.h"
#include "ui/combobox.h"
namespace app {
class BestFitCriteriaSelector : public ui::ComboBox {
public:
BestFitCriteriaSelector();
doc::FitCriteria criteria();
void criteria(doc::FitCriteria criteria);
};
} // namespace app
#endif

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -1692,7 +1692,8 @@ bool ColorBar::onCopy(Context* ctx)
return true;
}
bool ColorBar::onPaste(Context* ctx)
bool ColorBar::onPaste(Context* ctx,
const gfx::Point* position)
{
if (m_tilemapMode == TilemapMode::Tiles) {
showRemapTiles();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -92,6 +92,9 @@ namespace app {
TilemapMode tilemapMode() const;
void setTilemapMode(TilemapMode mode);
void lockTilemapMode() { m_tilesButton.setEnabled(false); };
void unlockTilemapMode() { m_tilesButton.setEnabled(true); };
bool isTilemapModeLocked() const { return !m_tilesButton.isEnabled(); };
TilesetMode tilesetMode() const;
void setTilesetMode(TilesetMode mode);
@ -116,7 +119,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -1576,11 +1576,13 @@ private:
item->setSelected(false);
Menu menu;
MenuItem
reset(Strings::symmetry_reset_position());
menu.addChild(&reset);
MenuItem resetToCenter(Strings::symmetry_reset_position());
MenuItem resetToViewCenter(Strings::symmetry_reset_position_to_view_center());
reset.Click.connect(
menu.addChild(&resetToCenter);
menu.addChild(&resetToViewCenter);
resetToCenter.Click.connect(
[doc, &docPref]{
docPref.symmetry.xAxis(doc->sprite()->width()/2.0);
docPref.symmetry.yAxis(doc->sprite()->height()/2.0);
@ -1588,6 +1590,18 @@ private:
doc->notifyGeneralUpdate();
});
resetToViewCenter.Click.connect(
[doc, &docPref]{
auto* editor = Editor::activeEditor();
const gfx::Rect& bounds = editor->getViewportBounds();
double xViewPosition = bounds.x + bounds.w/2.0;
double yViewPosition = bounds.y + bounds.h/2.0;
docPref.symmetry.xAxis(xViewPosition);
docPref.symmetry.yAxis(yViewPosition);
// Redraw symmetry rules
doc->notifyGeneralUpdate();
});
menu.showPopup(gfx::Point(bounds.x, bounds.y2()),
display());
}

View File

@ -58,7 +58,6 @@ public:
: m_session(session)
, m_backup(backup)
, m_task(nullptr) {
updateText();
}
crash::Session* session() const { return m_session; }
@ -150,6 +149,15 @@ public:
}
private:
void onPaint(PaintEvent& ev) override {
// The text is lazily initialized. So we read the backup data only
// when we have to show its information.
if (text().empty()) {
updateText();
}
ListItem::onPaint(ev);
}
void onSizeHint(SizeHintEvent& ev) override {
ListItem::onSizeHint(ev);
gfx::Size sz = ev.sizeHint();

View File

@ -589,12 +589,13 @@ bool DocView::onCopy(Context* ctx)
return false;
}
bool DocView::onPaste(Context* ctx)
bool DocView::onPaste(Context* ctx,
const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::Image ||
clipboard->format() == ClipboardFormat::Tilemap) {
clipboard->paste(ctx, true);
clipboard->paste(ctx, true, position);
return true;
}
else

View File

@ -102,7 +102,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -162,6 +162,8 @@ bool DraggingValueState::onUpdateStatusBar(Editor* editor)
void DraggingValueState::onBeforeCommandExecution(CommandExecutionEvent& ev)
{
if (m_editor->hasCapture())
m_editor->releaseMouse();
m_editor->backToPreviousState();
}

View File

@ -2658,7 +2658,9 @@ void Editor::setZoomAndCenterInMouse(const Zoom& zoom,
}
}
void Editor::pasteImage(const Image* image, const Mask* mask)
void Editor::pasteImage(const Image* image,
const Mask* mask,
const gfx::Point* position)
{
ASSERT(image);
@ -2690,11 +2692,14 @@ void Editor::pasteImage(const Image* image, const Mask* mask)
Sprite* sprite = this->sprite();
// Check bounds where the image will be pasted.
int x = mask->bounds().x;
int y = mask->bounds().y;
int x = (position ? position->x : mask->bounds().x);
int y = (position ? position->y : mask->bounds().y);
{
const Rect visibleBounds = getViewportBounds();
const Point maskCenter = mask->bounds().center();
const Point maskCenter = mask->bounds().center() +
(position ? gfx::Point(position->x - mask->bounds().x,
position->y - mask->bounds().y)
: gfx::Point());
// If the pasted image original location center point isn't
// visible, we center the image in the editor's visible bounds.
@ -2747,7 +2752,8 @@ void Editor::pasteImage(const Image* image, const Mask* mask)
m_brushPreview.hide();
Mask mask2(*mask);
mask2.setOrigin(x, y);
position ? mask2.setOrigin(position->x, position->y)
: mask2.setOrigin(x, y);
PixelsMovementPtr pixelsMovement(
new PixelsMovement(UIContext::instance(), site,

View File

@ -252,7 +252,9 @@ namespace app {
void setZoomAndCenterInMouse(const render::Zoom& zoom,
const gfx::Point& mousePos, ZoomBehavior zoomBehavior);
void pasteImage(const Image* image, const Mask* mask = nullptr);
void pasteImage(const Image* image,
const Mask* mask = nullptr,
const gfx::Point* position = nullptr);
void startSelectionTransformation(const gfx::Point& move, double angle);
void startFlipTransformation(doc::algorithm::FlipType flipType);

View File

@ -736,6 +736,10 @@ void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev)
return;
}
}
else if (command->id() == CommandId::ToggleTilesMode()) {
ev.cancel();
return;
}
if (m_pixelsMovement)
dropPixels();

View File

@ -43,6 +43,7 @@
#include "doc/layer.h"
#include "doc/mask.h"
#include "doc/sprite.h"
#include "doc/util.h"
#include "gfx/region.h"
#include "render/render.h"
@ -131,6 +132,10 @@ PixelsMovement::PixelsMovement(
, m_fastMode(false)
, m_needsRotSpriteRedraw(false)
{
// Save and Lock the TilemapMode.
// TODO: enable TilemapMode exchanges during PixelMovement.
if (m_site.layer()->isTilemap() && ColorBar::instance())
ColorBar::instance()->lockTilemapMode();
const float cornerThick = (m_site.tilemapMode() == TilemapMode::Tiles ?
CORNER_THICK_FOR_TILEMAP_MODE:
CORNER_THICK_FOR_PIXELS_MODE);
@ -172,6 +177,12 @@ PixelsMovement::PixelsMovement(
}
}
PixelsMovement::~PixelsMovement()
{
if (ColorBar::instance())
ColorBar::instance()->unlockTilemapMode();
}
bool PixelsMovement::editMultipleCels() const
{
return
@ -281,6 +292,11 @@ void PixelsMovement::setTransformationBase(const Transformation& t)
fullBounds |= gfx::Rect((int)newCorners[i].x, (int)newCorners[i].y, 1, 1);
}
// This align is done to properly invalidate regions on the editor when
// partial tiles are selected in the transform bounds
if (m_site.tilemapMode() == TilemapMode::Tiles)
fullBounds = m_site.grid().alignBounds(fullBounds);
// If "fullBounds" is empty is because the cel was not moved
if (!fullBounds.isEmpty()) {
// Notify the modified region.
@ -370,6 +386,7 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
gfx::RectF bounds = m_initialData.bounds();
gfx::PointF abs_initial_pivot = m_initialData.pivot();
gfx::PointF abs_pivot = m_currentData.pivot();
const bool tilesModeOn = (m_site.tilemapMode() == TilemapMode::Tiles);
auto newTransformation = m_currentData;
@ -377,7 +394,25 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
case MovePixelsHandle: {
double dx, dy;
if ((moveModifier & FineControl) == 0) {
if (tilesModeOn) {
if (m_catchPos.x == 0 && m_catchPos.y == 0) {
// Movement through keyboard:
dx = (pos.x - m_catchPos.x) * m_site.gridBounds().w;
dy = (pos.y - m_catchPos.y) * m_site.gridBounds().h;
}
else {
// Movement through mouse/trackpad:
const int gridW = m_site.gridBounds().w;
const int gridH = m_site.gridBounds().h;
gfx::PointF point(
snap_to_grid(gfx::Rect(0, 0, gridW, gridH),
(gfx::Point)(pos - m_catchPos),
PreferSnapTo::ClosestGridVertex));
dx = point.x;
dy = point.y;
}
}
else if ((moveModifier & FineControl) == 0) {
dx = (std::floor(pos.x) - std::floor(m_catchPos.x));
dy = (std::floor(pos.y) - std::floor(m_catchPos.y));
}
@ -395,13 +430,12 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
bounds.offset(dx, dy);
if ((m_site.tilemapMode() == TilemapMode::Tiles) ||
if (!tilesModeOn &&
(moveModifier & SnapToGridMovement) == SnapToGridMovement) {
// Snap the x1,y1 point to the grid.
gfx::Rect gridBounds = m_site.gridBounds();
gfx::PointF gridOffset(
snap_to_grid(
gridBounds,
m_site.gridBounds(),
gfx::Point(bounds.origin()),
PreferSnapTo::ClosestGridVertex));
@ -411,9 +445,7 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
}
newTransformation.bounds(bounds);
newTransformation.pivot(abs_initial_pivot +
bounds.origin() -
m_initialData.bounds().origin());
newTransformation.pivot(abs_initial_pivot + gfx::PointF(dx, dy));
break;
}
@ -496,10 +528,18 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
}
// Snap to grid when resizing tilemaps
if (m_site.tilemapMode() == TilemapMode::Tiles) {
if (tilesModeOn) {
// 'a' is a point in the top-left corner that is inside bounds
// unless the corners are inverted (a > b)
a.x = a.x - (a.x > b.x? 1 : 0);
a.y = a.y - (a.y > b.y? 1 : 0);
// 'b' is a point in the lower-right corner that is out of bounds by 1 unit
// unless the corners are inverted (a > b)
b.x = b.x - (a.x <= b.x? 1 : 0);
b.y = b.y - (a.y <= b.y? 1 : 0);
gfx::Rect gridBounds = m_site.gridBounds();
a = gfx::PointF(snap_to_grid(gridBounds, gfx::Point(a), PreferSnapTo::BoxOrigin));
b = gfx::PointF(snap_to_grid(gridBounds, gfx::Point(b), PreferSnapTo::BoxOrigin));
b = gfx::PointF(snap_to_grid(gridBounds, gfx::Point(b), PreferSnapTo::BoxEnd));
}
// Do not use "gfx::Rect(a, b)" here because if a > b we want to
@ -521,7 +561,7 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
case RotateSEHandle: {
// Cannot rotate tiles
// TODO add support to rotate tiles in straight angles (changing tile flags)
if (m_site.tilemapMode() == TilemapMode::Tiles)
if (tilesModeOn)
break;
double da = (std::atan2((double)(-pos.y + abs_pivot.y),
@ -564,7 +604,7 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
// Cannot skew tiles
// TODO could we support to skew tiles if we have the set of tiles (e.g. diagonals)?
// maybe too complex to implement in UI terms
if (m_site.tilemapMode() == TilemapMode::Tiles)
if (tilesModeOn)
break;
// u
@ -703,7 +743,7 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
case PivotHandle: {
// Calculate the new position of the pivot
gfx::PointF newPivot = m_initialData.pivot() + gfx::Point(pos) - m_catchPos;
gfx::PointF newPivot = m_initialData.pivot() + pos - m_catchPos;
newTransformation = m_initialData;
newTransformation.displacePivotTo(newPivot);
break;
@ -716,6 +756,8 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage,
std::unique_ptr<Mask>& outputMask)
{
// Absurd situation: tilemapMode == Tiles and current layer isn't a tilemap
ASSERT(m_site.tilemapMode() == TilemapMode::Pixels || m_site.layer()->isTilemap());
gfx::Rect bounds = m_currentData.transformedBounds();
if (bounds.isEmpty())
return;
@ -763,6 +805,39 @@ void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage,
outputMask.reset(mask.release());
}
void PixelsMovement::alignMasksAndTransformData(
const Mask* initialMask0,
const Mask* initialMask,
const Mask* currentMask,
const Transformation* initialData,
const Transformation* currentData,
const doc::Grid& grid,
const gfx::Size& deltaA,
const gfx::Size& deltaB)
{
m_initialMask0->replace(make_aligned_mask(&grid, initialMask0));
m_initialMask->replace(make_aligned_mask(&grid, initialMask));
m_currentMask->replace(make_aligned_mask(&grid, currentMask));
m_initialData = *initialData;
m_initialData.bounds(m_initialMask0->bounds());
m_currentData = *currentData;
// Raw grid alignment of currentData can result in unintentional scaling.
// That's why we need to know if the artist's intention was just to move
// the selection and/or scaling via 'initialDeltaA' and 'initialDeltaB'.
const gfx::Point currentDataAlignedOrigin =
grid.alignBounds(gfx::Rect(m_initialData.bounds().x + deltaA.w,
m_initialData.bounds().y + deltaA.h,
1, 1)).origin();
int deltaH = deltaB.w - deltaA.w;
int deltaV = deltaB.h - deltaA.h;
const gfx::RectF currentDataBounds(
currentDataAlignedOrigin.x,
currentDataAlignedOrigin.y,
m_initialData.bounds().w + deltaH,
m_initialData.bounds().h + deltaV);
m_currentData.bounds(currentDataBounds);
}
void PixelsMovement::stampImage()
{
stampImage(false);
@ -796,6 +871,42 @@ void PixelsMovement::stampImage(bool finalStamp)
stampExtraCelImage();
}
// Saving original values before the 'for' loop and the
// 'reproduceAllTransformationsWithInnerCmds' function for restoring later.
// All values of m_initialXX, m_currentXX will be recalculated
// to align their original selection bounds with each cel's grid.
const TilemapMode originalSiteTilemapMode = (
m_site.tilemapMode() == TilemapMode::Tiles &&
m_site.layer()->isTilemap()? TilemapMode::Tiles : TilemapMode::Pixels);
const TilesetMode originalSiteTilesetMode = m_site.tilesetMode();
const Mask initialMask0(*m_initialMask0);
const Mask initialMask(*m_initialMask);
const Mask currentMask(*m_currentMask);
auto initialData = m_initialData;
auto currentData = m_currentData;
// We need a way to know if 'a' or 'b' corners has changed
// as result of a scaling or moving command to replicate the intention on
// the other layers according the original mask (which can be aligned or
// not to the tilemap grid)
//
// a ----
// | |
// | |
// ---- b
const gfx::Rect currentAlignedBounds(
m_site.grid().alignBounds(currentData.bounds()));
const gfx::Rect initialAlignedBounds(
m_site.grid().alignBounds(initialMask.bounds()));
const gfx::Size deltaA(currentAlignedBounds.origin().x -
initialAlignedBounds.origin().x,
currentAlignedBounds.origin().y -
initialAlignedBounds.origin().y);
const gfx::Size deltaB(currentAlignedBounds.x2() -
initialAlignedBounds.x2(),
currentAlignedBounds.y2() -
initialAlignedBounds.y2());
for (Cel* target : cels) {
// We'll re-create the transformation for the other cels
if (target != currentCel) {
@ -803,7 +914,36 @@ void PixelsMovement::stampImage(bool finalStamp)
m_site.layer(target->layer());
m_site.frame(target->frame());
ASSERT(m_site.cel() == target);
Grid targetGrid(m_site.grid());
// Align masks and transformData before to 'reproduceAllTransformationsWithInnerCmds'
// Note: this alignement is needed only when the editor is on 'TilemapMode::Tiles',
// on the other hand 'TilemapMode::Pixels' do not require any additional
// mask/transformData adjustments.
if (originalSiteTilemapMode == TilemapMode::Tiles) {
if (target->layer()->isTilemap()) {
alignMasksAndTransformData(&initialMask0,
&initialMask,
&currentMask,
&initialData,
&currentData,
targetGrid,
deltaA,
deltaB);
m_site.tilemapMode(TilemapMode::Tiles);
}
else {
m_initialMask0->replace(initialMask0);
m_initialMask->replace(initialMask);
m_currentMask->replace(currentMask);
m_initialData.bounds(initialData.bounds());
m_currentData.bounds(currentData.bounds());
m_site.tilemapMode(TilemapMode::Pixels);
}
}
else {
m_site.tilemapMode(TilemapMode::Pixels);
m_site.tilesetMode(TilesetMode::Auto);
}
reproduceAllTransformationsWithInnerCmds();
}
@ -811,12 +951,20 @@ void PixelsMovement::stampImage(bool finalStamp)
stampExtraCelImage();
}
m_initialMask0->replace(initialMask0);
m_initialMask->replace(initialMask);
m_currentMask->replace(currentMask);
m_initialData.bounds(initialData.bounds());
m_currentData.bounds(currentData.bounds());
m_site.tilesetMode(originalSiteTilesetMode);
currentCel = m_site.cel();
if (currentCel &&
(m_site.layer() != currentCel->layer() ||
m_site.frame() != currentCel->frame())) {
m_site.layer(currentCel->layer());
m_site.frame(currentCel->frame());
m_site.tilemapMode(originalSiteTilemapMode);
m_site.tilesetMode(originalSiteTilesetMode);
redrawExtraImage();
}
}
@ -1000,6 +1148,7 @@ void PixelsMovement::redrawExtraImage(Transformation* transformation)
if (m_site.tilemapMode() == TilemapMode::Tiles) {
// Transforming tiles
extraCelSize = m_site.grid().canvasToTile(bounds).size();
bounds = m_site.grid().alignBounds(bounds);
}
else {
// Transforming pixels
@ -1046,7 +1195,8 @@ void PixelsMovement::drawImage(
auto corners = transformation.transformedCorners();
gfx::Rect bounds = corners.bounds(transformation.cornerThick());
if (m_site.tilemapMode() == TilemapMode::Tiles) {
if (m_site.tilemapMode() == TilemapMode::Tiles &&
m_site.layer()->isTilemap()) {
dst->setMaskColor(doc::notile);
dst->clear(dst->maskColor());
@ -1408,10 +1558,16 @@ void PixelsMovement::reproduceAllTransformationsWithInnerCmds()
m_document->setMask(m_initialMask0.get());
m_initialMask->copyFrom(m_initialMask0.get());
m_originalImage.reset(
new_image_from_mask(
m_site, m_initialMask.get(),
Preferences::instance().experimental.newBlend()));
if (m_site.layer()->isTilemap() && m_site.tilemapMode() == TilemapMode::Tiles) {
m_originalImage.reset(
new_tilemap_from_mask(
m_site, m_initialMask.get()));
}
else
m_originalImage.reset(
new_image_from_mask(
m_site, m_initialMask.get(),
Preferences::instance().experimental.newBlend()));
for (const InnerCmd& c : m_innerCmds) {
switch (c.type) {

View File

@ -74,6 +74,7 @@ namespace app {
const Image* moveThis,
const Mask* mask,
const char* operationName);
~PixelsMovement();
const Site& site() { return m_site; }
@ -161,6 +162,16 @@ namespace app {
const double angle);
CelList getEditableCels();
void reproduceAllTransformationsWithInnerCmds();
void alignMasksAndTransformData(const Mask* initialMask0,
const Mask* initialMask,
const Mask* currentMask,
const Transformation* initialData,
const Transformation* currentData,
const doc::Grid& grid,
const gfx::Size& deltaA,
const gfx::Size& deltaB);
#if _DEBUG
void dumpInnerCmds();
#endif

View File

@ -256,10 +256,8 @@ FontPopup::FontPopup(const FontInfo& fontInfo)
// directories (fontDirs)
base::paths files;
for (const auto& fontDir : fontDirs) {
for (const auto& file : base::list_files(fontDir)) {
std::string fullpath = base::join_path(fontDir, file);
if (base::is_file(fullpath))
files.push_back(fullpath);
for (const auto& file : base::list_files(fontDir, base::ItemType::Files)) {
files.push_back(base::join_path(fontDir, file));
}
}

View File

@ -230,7 +230,8 @@ bool HomeView::onCopy(Context* ctx)
return false;
}
bool HomeView::onPaste(Context* ctx)
bool HomeView::onPaste(Context* ctx,
const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::Image) {

View File

@ -74,7 +74,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -87,10 +88,11 @@ void InputChain::copy(Context* ctx)
}
}
void InputChain::paste(Context* ctx)
void InputChain::paste(Context* ctx,
const gfx::Point* position)
{
for (auto e : m_elements) {
if (e->onCanPaste(ctx) && e->onPaste(ctx))
if (e->onCanPaste(ctx) && e->onPaste(ctx, position))
break;
}
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -8,6 +9,8 @@
#define APP_INPUT_CHAIN_H_INCLUDED
#pragma once
#include "gfx/point.h"
#include <vector>
namespace ui {
@ -35,7 +38,8 @@ namespace app {
void cut(Context* ctx);
void copy(Context* ctx);
void paste(Context* ctx);
void paste(Context* ctx,
const gfx::Point* position);
void clear(Context* ctx);
void cancel(Context* ctx);

View File

@ -9,6 +9,8 @@
#define APP_INPUT_CHAIN_ELEMENT_H_INCLUDED
#pragma once
#include "gfx/point.h"
namespace ui {
class Message;
}
@ -34,7 +36,8 @@ namespace app {
// which catch any exception that is thrown.
virtual bool onCut(Context* ctx) = 0;
virtual bool onCopy(Context* ctx) = 0;
virtual bool onPaste(Context* ctx) = 0;
virtual bool onPaste(Context* ctx,
const gfx::Point* position) = 0;
virtual bool onClear(Context* ctx) = 0;
virtual void onCancel(Context* ctx) = 0;
};

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -138,6 +138,7 @@ namespace app {
const KeyboardShortcuts& globalKeys) const;
bool isPressed() const;
bool isLooselyPressed() const;
bool isListed() const;
bool hasAccel(const ui::Accelerator& accel) const;
bool hasUserDefinedAccels() const;

View File

@ -494,6 +494,11 @@ bool Key::isLooselyPressed() const
return false;
}
bool Key::isListed() const
{
return type() != KeyType::Command || !command()->isListed(params());
}
bool Key::hasAccel(const ui::Accelerator& accel) const
{
return accels().has(accel);

View File

@ -61,8 +61,8 @@ namespace app {
~SkinTheme();
const std::string& path() { return m_path; }
int preferredScreenScaling() { return m_preferredScreenScaling; }
int preferredUIScaling() { return m_preferredUIScaling; }
int preferredScreenScaling() const { return m_preferredScreenScaling; }
int preferredUIScaling() const { return m_preferredUIScaling; }
text::FontMgrRef fontMgr() const override { return m_fontMgr; }
text::Font* getDefaultFont() const override { return m_defaultFont.get(); }

View File

@ -4398,7 +4398,8 @@ bool Timeline::onCopy(Context* ctx)
return false;
}
bool Timeline::onPaste(Context* ctx)
bool Timeline::onPaste(Context* ctx,
const gfx::Point* position)
{
auto clipboard = ctx->clipboard();
if (clipboard->format() == ClipboardFormat::DocRange) {

View File

@ -190,7 +190,8 @@ namespace app {
bool onCanClear(Context* ctx) override;
bool onCut(Context* ctx) override;
bool onCopy(Context* ctx) override;
bool onPaste(Context* ctx) override;
bool onPaste(Context* ctx,
const gfx::Point* position) override;
bool onClear(Context* ctx) override;
void onCancel(Context* ctx) override;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -381,12 +381,13 @@ bool Workspace::onCopy(Context* ctx)
return false;
}
bool Workspace::onPaste(Context* ctx)
bool Workspace::onPaste(Context* ctx,
const gfx::Point* position)
{
WorkspaceView* view = activeView();
InputChainElement* activeElement = (view ? view->onGetInputChainElement(): nullptr);
if (activeElement)
return activeElement->onPaste(ctx);
return activeElement->onPaste(ctx, position);
else
return false;
}

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