Revert Esc key behavior to drop+deselect (fix #5102)

This adds a new button in the context bar so we have the three
available options to handle a transformation/drop pixels:

* Drop pixels and deselect (Esc key)
* Drop pixels but keep the selection (Enter key), new "Apply" command
* Discard changes/undo (Ctrl+Z)

This adds a new key context (Transformation) and also fixes tooltip
shortcuts on context bar buttons to show the current configured
shortcut for each action.

Reverts debab653fa and 194f8424a8
This commit is contained in:
David Capello 2025-08-27 11:54:15 -03:00
parent 0c49f2d7ad
commit f61c2c3950
19 changed files with 204 additions and 180 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -331,6 +331,8 @@
<part id="flag_highlight" x="16" y="240" w="16" h="10"/>
<part id="drop_pixels_ok" x="176" y="176" w="7" h="8"/>
<part id="drop_pixels_ok_selected" x="176" y="184" w="7" h="8"/>
<part id="drop_pixels_drop" x="184" y="176" w="7" h="8"/>
<part id="drop_pixels_drop_selected" x="184" y="184" w="7" h="8"/>
<part id="drop_pixels_cancel" x="192" y="176" w="7" h="8"/>
<part id="drop_pixels_cancel_selected" x="192" y="184" w="7" h="8"/>
<part id="warning_box" x="112" y="80" w="9" h="10"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -327,6 +327,8 @@
<part id="flag_highlight" x="16" y="240" w="16" h="10"/>
<part id="drop_pixels_ok" x="176" y="176" w="7" h="8"/>
<part id="drop_pixels_ok_selected" x="176" y="184" w="7" h="8"/>
<part id="drop_pixels_drop" x="184" y="176" w="7" h="8"/>
<part id="drop_pixels_drop_selected" x="184" y="184" w="7" h="8"/>
<part id="drop_pixels_cancel" x="192" y="176" w="7" h="8"/>
<part id="drop_pixels_cancel_selected" x="192" y="184" w="7" h="8"/>
<part id="warning_box" x="112" y="80" w="9" h="10"/>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
<!-- Copyright (C) 2001-2018 David Capello -->
<gui>
<!-- Keyboard shortcuts -->
@ -371,6 +371,12 @@
<param name="quantity" value="1" />
</key>
<!-- Main selection actions (apply transformation / undo) -->
<key command="DeselectMask" shortcut="Esc" context="Transformation" />
<key command="Apply" shortcut="Enter" context="Transformation" />
<key command="Apply" shortcut="Enter Pad" context="Transformation" />
<key command="Undo" shortcut="Ctrl+Z" mac="Cmd+Z" context="Transformation" />
<!-- Move selection with arrows -->
<key command="MoveMask" shortcut="Left" context="Selection">
<param name="target" value="content" />

View File

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

View File

@ -206,6 +206,7 @@ AddColor_Background = Background
AddColor_Foreground = Foreground
AddColor_Specific = Specific
AdvancedMode = Advanced Mode
Apply = Apply
AutocropSprite = Trim Sprite
AutocropSprite_ByGrid = Trim Sprite by Grid
BackgroundFromLayer = Background from Layer
@ -554,8 +555,6 @@ amount = Amount:
flatten = Merge layers
[context_bar]
discard_changes = Discard Changes
deselect = Deselect
center = Center
fit_screen = Fit Screen
back = Back
@ -583,8 +582,9 @@ rotsprite = RotSprite
pixel_perfect = Pixel-perfect
linear_gradient = Linear Gradient
radial_gradient = Radial Gradient
drop_pixel = Drop pixels here (Enter)
cancel_drag = Cancel drag and drop (Esc)\nRight-click: Configure action
drop_pixel_and_deselect = Apply transformation and deselect
drop_pixel = Apply transformation and keep selection
cancel_drag = Cancel transformation and undo/discard changes
auto_select_layer = Auto Select Layer
all = All
none = None
@ -967,6 +967,7 @@ key_context_move_tool = Move Tool
key_context_freehand_tool = Freehand Tool
key_context_shape_tool = Shape Tool
key_context_frames_selection = Frames Selection
key_context_transformation = Transformation
copy_selection = Copy Selection
snap_to_grid = Snap To Grid
lock_axis = Lock Axis

View File

@ -74,7 +74,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${output_fn}.tmp ${output_fn}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
MAIN_DEPENDENCY ${strings_en_ini}
DEPENDS ${GEN_DEP})
DEPENDS ${GEN_DEP} commands/commands_list.h)
list(APPEND generated_files ${output_fn})
# Check translations
@ -368,6 +368,7 @@ target_sources(app-lib PRIVATE
color_picker.cpp
color_spaces.cpp
color_utils.cpp
commands/apply.cpp
commands/cmd_about.cpp
commands/cmd_add_color.cpp
commands/cmd_advanced_mode.cpp

View File

@ -0,0 +1,46 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/commands/command.h"
#include "app/context.h"
#include "app/ui/editor/editor.h"
namespace app {
// Depends on the current context/state, used to apply the current
// transformation (drop pixels).
class ApplyCommand : public Command {
public:
ApplyCommand();
protected:
void onExecute(Context* ctx) override;
};
ApplyCommand::ApplyCommand() : Command(CommandId::Apply(), CmdUIOnlyFlag)
{
}
void ApplyCommand::onExecute(Context* ctx)
{
if (!ctx->isUIAvailable())
return;
auto* editor = Editor::activeEditor();
if (editor && editor->isMovingPixels())
editor->dropMovingPixels();
}
Command* CommandFactory::createApplyCommand()
{
return new ApplyCommand;
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -8,6 +8,7 @@
FOR_EACH_COMMAND(About)
FOR_EACH_COMMAND(AddColor)
FOR_EACH_COMMAND(AdvancedMode)
FOR_EACH_COMMAND(Apply)
FOR_EACH_COMMAND(AutocropSprite)
FOR_EACH_COMMAND(BackgroundFromLayer)
FOR_EACH_COMMAND(BrightnessContrast)

View File

@ -681,81 +681,89 @@ void CustomizedGuiManager::onNewDisplayConfiguration(Display* display)
bool CustomizedGuiManager::processKey(Message* msg)
{
App* app = App::instance();
const KeyContext currentCtx = KeyboardShortcuts::getCurrentKeyContext();
const KeyboardShortcuts* keys = KeyboardShortcuts::instance();
const KeyContext contexts[] = { KeyboardShortcuts::getCurrentKeyContext(), KeyContext::Normal };
const KeyContext contexts[] = { currentCtx, KeyContext::Normal };
int n = (contexts[0] != contexts[1] ? 2 : 1);
// Find best match (prefer the shortcut that matches the context first)
KeyPtr key = nullptr;
for (int i = 0; i < n; ++i) {
for (const KeyPtr& key : *keys) {
if (key->isPressed(msg, contexts[i])) {
// Cancel menu-bar loops (to close any popup menu)
app->mainWindow()->getMenuBar()->cancelMenuLoop();
switch (key->type()) {
case KeyType::Tool: {
tools::Tool* current_tool = app->activeTool();
tools::Tool* select_this_tool = key->tool();
tools::ToolBox* toolbox = app->toolBox();
std::vector<tools::Tool*> possibles;
// Collect all tools with the pressed keyboard-shortcut
for (tools::Tool* tool : *toolbox) {
const KeyPtr key = keys->tool(tool);
if (key && key->isPressed(msg))
possibles.push_back(tool);
}
if (possibles.size() >= 2) {
bool done = false;
for (size_t i = 0; i < possibles.size(); ++i) {
if (possibles[i] != current_tool &&
ToolBar::instance()->isToolVisible(possibles[i])) {
select_this_tool = possibles[i];
done = true;
break;
}
}
if (!done) {
for (size_t i = 0; i < possibles.size(); ++i) {
// If one of the possibilities is the current tool
if (possibles[i] == current_tool) {
// We select the next tool in the possibilities
select_this_tool = possibles[(i + 1) % possibles.size()];
break;
}
}
}
}
ToolBar::instance()->selectTool(select_this_tool);
return true;
}
case KeyType::Command: {
Command* command = key->command();
// Commands are executed only when the main window is
// the current window running.
if (getForegroundWindow() == app->mainWindow()) {
// OK, so we can execute the command represented
// by the pressed-key in the message...
UIContext::instance()->executeCommandFromMenuOrShortcut(command, key->params());
return true;
}
break;
}
case KeyType::Quicktool: {
// Do nothing, it is used in the editor through the
// KeyboardShortcuts::getCurrentQuicktool() function.
break;
}
}
break;
for (const KeyPtr& k : *keys) {
if (k->isPressed(msg, contexts[i]) &&
(!key || (key->keycontext() != currentCtx && k->keycontext() == currentCtx))) {
key = k;
}
}
}
if (!key)
return false;
// Cancel menu-bar loops (to close any popup menu)
app->mainWindow()->getMenuBar()->cancelMenuLoop();
switch (key->type()) {
case KeyType::Tool: {
tools::Tool* current_tool = app->activeTool();
tools::Tool* select_this_tool = key->tool();
tools::ToolBox* toolbox = app->toolBox();
std::vector<tools::Tool*> possibles;
// Collect all tools with the pressed keyboard-shortcut
for (tools::Tool* tool : *toolbox) {
const KeyPtr key = keys->tool(tool);
if (key && key->isPressed(msg))
possibles.push_back(tool);
}
if (possibles.size() >= 2) {
bool done = false;
for (size_t i = 0; i < possibles.size(); ++i) {
if (possibles[i] != current_tool && ToolBar::instance()->isToolVisible(possibles[i])) {
select_this_tool = possibles[i];
done = true;
break;
}
}
if (!done) {
for (size_t i = 0; i < possibles.size(); ++i) {
// If one of the possibilities is the current tool
if (possibles[i] == current_tool) {
// We select the next tool in the possibilities
select_this_tool = possibles[(i + 1) % possibles.size()];
break;
}
}
}
}
ToolBar::instance()->selectTool(select_this_tool);
return true;
}
case KeyType::Command: {
Command* command = key->command();
// Commands are executed only when the main window is
// the current window running.
if (getForegroundWindow() == app->mainWindow()) {
// OK, so we can execute the command represented
// by the pressed-key in the message...
UIContext::instance()->executeCommandFromMenuOrShortcut(command, key->params());
return true;
}
break;
}
case KeyType::Quicktool: {
// Do nothing, it is used in the editor through the
// KeyboardShortcuts::getCurrentQuicktool() function.
break;
}
}
return false;
}

View File

@ -388,7 +388,6 @@ FOR_ENUM(app::gen::TimelinePosition)
FOR_ENUM(app::gen::ToGrayAlgorithm)
FOR_ENUM(app::gen::WindowColorProfile)
FOR_ENUM(app::gen::AlphaRange)
FOR_ENUM(app::gen::CancelSelection)
FOR_ENUM(app::tools::ColorFromTo)
FOR_ENUM(app::tools::DynamicSensor)
FOR_ENUM(app::tools::FreehandAlgorithm)

View File

@ -1390,11 +1390,12 @@ public:
class ContextBar::DropPixelsField : public ButtonSet {
public:
DropPixelsField() : ButtonSet(2)
DropPixelsField() : ButtonSet(3)
{
auto* theme = SkinTheme::get(this);
addItem(theme->parts.dropPixelsOk(), theme->styles.contextBarButton());
addItem(theme->parts.dropPixelsDrop(), theme->styles.contextBarButton());
addItem(theme->parts.dropPixelsCancel(), theme->styles.contextBarButton());
setOfferCapture(false);
}
@ -1402,12 +1403,29 @@ public:
void setupTooltips(TooltipManager* tooltipManager)
{
// TODO Enter and Esc should be configurable keys
tooltipManager->addTooltipFor(at(0), Strings::context_bar_drop_pixel(), BOTTOM);
tooltipManager->addTooltipFor(at(1), Strings::context_bar_cancel_drag(), BOTTOM);
tooltipManager->addTooltipFor(
at(0),
key_tooltip(Strings::context_bar_drop_pixel_and_deselect().c_str(),
CommandId::DeselectMask(),
{},
KeyContext::Transformation),
BOTTOM);
tooltipManager->addTooltipFor(at(1),
key_tooltip(Strings::context_bar_drop_pixel().c_str(),
CommandId::Apply(),
{},
KeyContext::Transformation),
BOTTOM);
tooltipManager->addTooltipFor(at(2),
key_tooltip(Strings::context_bar_cancel_drag().c_str(),
CommandId::Undo(),
{},
KeyContext::Transformation),
BOTTOM);
}
obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
obs::signal<void(ContextBarObserver::DropAction, const gfx::Point&)> ConfigureDropPixels;
protected:
void onItemChange(Item* item) override
@ -1415,25 +1433,11 @@ protected:
ButtonSet::onItemChange(item);
switch (selectedItem()) {
case 0: DropPixels(ContextBarObserver::DropPixels); break;
case 1: DropPixels(ContextBarObserver::CancelDrag); break;
case 0: DropPixels(ContextBarObserver::Deselect); break;
case 1: DropPixels(ContextBarObserver::DropPixels); break;
case 2: DropPixels(ContextBarObserver::CancelDrag); break;
}
}
void onRightClick(Item* item) override
{
ButtonSet::onRightClick(item);
const gfx::Rect rc = item->bounds();
const gfx::Point pt(rc.x, rc.y2());
auto action = ContextBarObserver::DropPixels;
switch (selectedItem()) {
case 0: action = ContextBarObserver::DropPixels; break;
case 1: action = ContextBarObserver::CancelDrag; break;
}
ConfigureDropPixels(action, pt);
}
};
class ContextBar::EyedropperField : public HBox {
@ -1968,8 +1972,6 @@ ContextBar::ContextBar(TooltipManager* tooltipManager, ColorBar* colorBar)
m_keysConn = KeyboardShortcuts::instance()->UserChange.connect(
[this, tooltipManager] { setupTooltips(tooltipManager); });
m_dropPixelsConn = m_dropPixels->DropPixels.connect(&ContextBar::onDropPixels, this);
m_configureDropPixelsConn =
m_dropPixels->ConfigureDropPixels.connect(&ContextBar::onConfigureDropPixels, this);
setActiveBrush(createBrushFromPreferences());
@ -2114,14 +2116,6 @@ void ContextBar::onDropPixels(ContextBarObserver::DropAction action)
notify_observers(&ContextBarObserver::onDropPixels, action);
}
void ContextBar::onConfigureDropPixels(ContextBarObserver::DropAction action, const gfx::Point& pt)
{
notify_observers<ContextBarObserver::DropAction, const gfx::Point&>(
&ContextBarObserver::onConfigureDropPixels,
action,
pt);
}
void ContextBar::updateSliceFields(const Site& site)
{
if (site.sprite())

View File

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

View File

@ -9,17 +9,14 @@
#define APP_CONTEXT_BAR_OBSERVER_H_INCLUDED
#pragma once
#include "gfx/fwd.h"
namespace app {
class ContextBarObserver {
public:
enum DropAction { DropPixels, CancelDrag };
enum DropAction { Deselect, DropPixels, CancelDrag };
virtual ~ContextBarObserver() {}
virtual void onDropPixels(DropAction action) {}
virtual void onConfigureDropPixels(DropAction action, const gfx::Point& pt) {}
};
} // namespace app

View File

@ -470,16 +470,6 @@ bool MovingPixelsState::onKeyDown(Editor* editor, KeyMessage* msg)
// FineControl now (e.g. if we pressed another modifier key).
m_lockedKeyAction = KeyAction::None;
// TODO make these keys customizable
if (msg->scancode() == kKeyEsc) {
cancelDrag();
return true;
}
if (msg->scancode() == kKeyEnter || msg->scancode() == kKeyEnterPad) {
dropPixels();
return true;
}
// Use StandbyState implementation
return StandbyState::onKeyDown(editor, msg);
}
@ -630,6 +620,12 @@ void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev)
return;
}
}
// Handle undo directly as cancelDrag() to avoid adding an action in the history.
else if (command->id() == CommandId::Undo()) {
cancelDrag();
ev.cancel();
return;
}
// Don't drop pixels if the user zooms/scrolls/picks a color
// using commands.
else if ((command->id() == CommandId::Zoom()) || (command->id() == CommandId::Scroll()) ||
@ -791,66 +787,33 @@ void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action)
return;
switch (action) {
case ContextBarObserver::Deselect: deselect(); break;
case ContextBarObserver::DropPixels: dropPixels(); break;
case ContextBarObserver::CancelDrag: cancelDrag(); break;
}
}
void MovingPixelsState::deselect()
{
if (!m_pixelsMovement || m_discarded)
return;
dropPixels();
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
}
void MovingPixelsState::cancelDrag()
{
if (!m_pixelsMovement || m_discarded)
return;
switch (Preferences::instance().selection.cancelSelection()) {
case gen::CancelSelection::DISCARD:
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
m_discarded = true;
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
m_discarded = true;
// Quit from MovingPixelsState, back to standby.
dropPixels();
break;
case gen::CancelSelection::DESELECT: {
dropPixels();
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
break;
}
}
}
void MovingPixelsState::onConfigureDropPixels(ContextBarObserver::DropAction action,
const gfx::Point& pt)
{
if (!isActiveEditor())
return;
switch (action) {
case ContextBarObserver::DropPixels:
// Do nothing
break;
case ContextBarObserver::CancelDrag: {
Menu menu;
MenuItem discardChanges(Strings::context_bar_discard_changes());
MenuItem deselect(Strings::context_bar_deselect());
menu.addChild(&discardChanges);
menu.addChild(&deselect);
auto& opt = Preferences::instance().selection.cancelSelection;
discardChanges.setSelected(opt() == gen::CancelSelection::DISCARD);
deselect.setSelected(opt() == gen::CancelSelection::DESELECT);
discardChanges.Click.connect([&opt] { opt(gen::CancelSelection::DISCARD); });
deselect.Click.connect([&opt] { opt(gen::CancelSelection::DESELECT); });
ContextBar* contextBar = App::instance()->contextBar();
menu.showPopup(pt, contextBar->display());
break;
}
}
// Quit from MovingPixelsState, back to standby.
dropPixels();
}
void MovingPixelsState::onPivotChange()

View File

@ -77,7 +77,6 @@ public:
// ContextBarObserver
void onDropPixels(ContextBarObserver::DropAction action) override;
void onConfigureDropPixels(ContextBarObserver::DropAction action, const gfx::Point& pt) override;
// PixelsMovementDelegate
void onPivotChange() override;
@ -95,6 +94,8 @@ private:
void onBeforeCommandExecution(CommandExecutionEvent& ev);
void setTransparentColor(bool opaque, const app::Color& color);
void deselect();
void dropPixels();
void cancelDrag();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2023 Igara Studio S.A.
// Copyright (C) 2023-2025 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -23,6 +23,7 @@ enum class KeyContext {
ShapeTool,
MouseWheel,
FramesSelection,
Transformation,
};
} // namespace app

View File

@ -22,6 +22,7 @@
#include "app/tools/ink.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
#include "app/ui/editor/editor.h"
#include "app/ui/key.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui_context.h"
@ -157,6 +158,7 @@ static struct {
{ "FreehandTool", app::KeyContext::FreehandTool },
{ "ShapeTool", app::KeyContext::ShapeTool },
{ "FramesSelection", app::KeyContext::FramesSelection },
{ "Transformation", app::KeyContext::Transformation },
{ NULL, app::KeyContext::Any }
};
@ -1078,6 +1080,12 @@ void KeyboardShortcuts::disableShortcut(const ui::Shortcut& shortcut,
// static
KeyContext KeyboardShortcuts::getCurrentKeyContext()
{
// For shortcuts to Apply/Cancel transformation/moving pixels state.
auto* editor = Editor::activeEditor();
if (editor && editor->isMovingPixels()) {
return KeyContext::Transformation;
}
auto* ctx = UIContext::instance();
Doc* doc = ctx->activeDocument();
if (doc && doc->isMaskVisible() &&
@ -1335,6 +1343,7 @@ std::string convertKeyContextToUserFriendlyString(KeyContext keyContext)
case KeyContext::FreehandTool: return I18N_KEY(key_context_freehand_tool);
case KeyContext::ShapeTool: return I18N_KEY(key_context_shape_tool);
case KeyContext::FramesSelection: return I18N_KEY(key_context_frames_selection);
case KeyContext::Transformation: return I18N_KEY(key_context_transformation);
}
return std::string();
}