Compare commits

...

23 Commits

Author SHA1 Message Date
drowley8 ca05df4213
Merge 56439067b7 into 107e846911 2025-10-01 21:10:11 -05:00
David Capello 107e846911 Remove direct TRACEARGS() calls
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-10-01 16:53:07 -03:00
David Capello 577caa4793 Reword action names when dropping files/images onto the timeline
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-09-30 08:26:44 -03:00
David Capello f3a372e78e Fix div by zero crash dropping image (fix #5447) 2025-09-30 08:24:50 -03:00
Joshua Lopez 41e5097c33 Fix mouse gestures not working after switching tabs (fix aseprite#4388)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-09-29 18:06:45 -03:00
Christian Kaiser cf290b7679 Check mask validity before pasting (fix #5361)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-09-29 11:32:39 -03:00
David Capello d7b2faca6d Fix crash painting deleted cels while applying filter (fix #4991)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
This could happen in the Editor::onPaint() or Timeline::onPaint().
2025-09-24 16:36:34 -03:00
David Capello f26ce64208 [lua] Update scripting API version to 36 2025-09-22 09:04:57 -03:00
David Capello e39dc90001 [lua] Enclose key names for plugin preferences with ["..."] when it's required (#5412) 2025-09-19 15:27:00 -03:00
Martín Capello 8f0dea0988 Avoid syntax errors reading extension preferences
Fix #5412 by using brackets and double quotes to enclose the name of
the preferences' properties and avoid issues when they contain unicode
chars
2025-09-19 14:58:35 -03:00
David Capello e8f18087e5 Update laf module 2025-09-18 20:47:56 -03:00
David Capello d8ac1a7a9e Update laf and add new libxrandr dependency 2025-09-17 09:15:35 -03:00
Aiden J Erickson 5cfa75ab3f [multimon] allow windows to open in non-default workareas (#4560, #5426)
Fix issue below:
In multi-monitor setups, native windows, e.g. the preview window, will,
when re-opened, be moved/clamped to the monitor of the main aseprite
window even if the re-opened window was previously and explicitly moved
to a different monitor.
2025-09-16 20:42:21 -03:00
David Capello c444b566e1 Fix painting UI theme colors as they are specified in sRGB color space
Fixes a regression found after merging #5414:
https://github.com/aseprite/aseprite/pull/5414#issuecomment-3286339563

We always expected a sRGB color in ui::Graphics API and we can specify
a color in another color space using the ui::Paint version of its
member functions.

Several functions related to color spaces are now using a ui::Display
to receive the specific display where we're going to paint, instead of
using os::System::instance()->defaultWindow()->colorSpace().
2025-09-16 09:42:35 -03:00
David Capello 5d5f3ec234 Convert AppShortcut in a ui::Shortcut subclass 2025-09-12 15:48:38 -03:00
David Capello 71e680e05a Refactor key context matching (fix #5390)
* Added AppShortcut::fitsBetterThan() to compare shortcuts and decide
  which one is better depending on the current key context and the ones
  where shortcuts are defined
* Added Key::fitsContext() to known if this key definition makes sense
  for a given current key context, e.g. Normal context can be used in
  SelectionTool context, and SelectionTool can be used in Transformation
* Added KeyboardShortcuts::findBestKeyFromMessage() to centralize the
  keyboard shortcuts matching moving some code from
  CustomizedGuiManager::processKey()
* Added more keyboard shortcuts tests (including some for #5390)
2025-09-12 15:41:08 -03:00
David Capello df5dcdc1d9 Add some KeyboardShortcuts basic tests 2025-09-12 15:07:26 -03:00
David Capello 85793a9e5f Add AppShortcut to include app::KeySource + ui::Shortcut
We need a list of shorcuts (AppShortcuts) similar to ui::Shortcuts but
including the key source to compare later where the shortcut was
defined and give more priority to the user-defined shortcuts. This
logic is not yet added, this patch is only a refactor.
2025-09-12 15:07:26 -03:00
David Capello d7b07a5173 Now Paint::color() can receive a color space w/any laf backend 2025-09-12 12:24:53 -03:00
Thorbjørn Lindeijer fae42dbe12 CMake: Use find_package(PkgConfig) instead of direct include
This gets rid of the following warning:

```
CMake Warning (dev) at /usr/share/cmake/Modules/FindPackageHandleStandardArgs.cmake:441 (message):
  The package name passed to `find_package_handle_standard_args` (PkgConfig)
  does not match the name of the calling package (HarfBuzz).  This can lead
  to problems in calling code that expects `find_package` result variables
  (e.g., `_FOUND`) to follow a certain pattern.
Call Stack (most recent call first):
  /usr/share/cmake/Modules/FindPkgConfig.cmake:114 (find_package_handle_standard_args)
  aseprite/cmake/FindHarfBuzz.cmake:33 (include)
  CMakeLists.txt:173 (find_package)
This warning is for project developers.  Use -Wno-dev to suppress it.
```

By applying the following change:
407fa892d9

The file could use further updates based on the upstream version.
2025-09-12 11:18:52 -03:00
David Capello 689e1ff524 Remove trailing whitespace from feature_request.md
This was the only file failing a pre-commit run --all-files
2025-09-11 17:59:13 -03:00
Gaspar Capello 907138d5dd Fix wrong color space in multiple UI elements
This fix solves color space missmatch in the following UI elements:
- Timeline thumbnails
- Palette entries
- Tileset entries
- Color Spectrum and Wheels
- Color Popup Gradients
- Color Button
2025-09-05 14:50:36 -03:00
drowley8 56439067b7
Add quotes to aseprite-thumbnailer
Without quotes, the thumbnailer could silently fail if the source or destination happens to have a space anywhere in the path, e.g. /home/myself/My Sprites/character.aseprite. The quotes help prevent this misunderstanding.
2025-04-28 14:56:47 -06:00
50 changed files with 699 additions and 257 deletions

View File

@ -24,7 +24,8 @@ jobs:
sudo apt-get update -qq
sudo apt-get install -y \
libpixman-1-dev libfreetype6-dev libharfbuzz-dev zlib1g-dev \
libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
libx11-dev libxcursor-dev libxi-dev libxrandr-dev libgl1-mesa-dev \
libfontconfig1-dev
- uses: aseprite/get-ninja@main
- uses: ilammy/msvc-dev-cmd@v1
if: runner.os == 'Windows'

View File

@ -28,7 +28,8 @@ jobs:
sudo apt-get update -qq
sudo apt-get install -y \
libpixman-1-dev libfreetype6-dev libharfbuzz-dev zlib1g-dev \
libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
libx11-dev libxcursor-dev libxi-dev libxrandr-dev libgl1-mesa-dev \
libfontconfig1-dev
- name: Install Skia
if: ${{ matrix.ui == 'gui' }}
shell: bash

View File

@ -25,7 +25,7 @@ jobs:
apt_packages: |
libc++-dev, libc++abi-dev, libpixman-1-dev,
libfreetype6-dev, libharfbuzz-dev, zlib1g-dev, libx11-dev,
libxcursor-dev, libxi-dev, libgl1-mesa-dev
libxcursor-dev, libxi-dev, libxrandr-dev, libgl1-mesa-dev
cmake_command: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DLAF_BACKEND=none -DCMAKE_EXPORT_COMPILE_COMMANDS=on
- uses: ZedThree/clang-tidy-review/upload@v0.20.1

View File

@ -74,7 +74,7 @@ On macOS you will need macOS 15.4 SDK and Xcode 16.3 (older versions might work)
You will need the following dependencies on Ubuntu/Debian:
sudo apt-get install -y g++ clang cmake ninja-build libx11-dev libxcursor-dev libxi-dev libgl1-mesa-dev libfontconfig1-dev
sudo apt-get install -y g++ clang cmake ninja-build libx11-dev libxcursor-dev libxi-dev libxrandr-dev libgl1-mesa-dev libfontconfig1-dev
Or use clang-12 packages (or newer) in case that clang in your distribution is older than clang 12.0:
@ -82,15 +82,15 @@ Or use clang-12 packages (or newer) in case that clang in your distribution is o
On Fedora:
sudo dnf install -y gcc-c++ clang libcxx-devel cmake ninja-build libX11-devel libXcursor-devel libXi-devel mesa-libGL-devel fontconfig-devel
sudo dnf install -y gcc-c++ clang libcxx-devel cmake ninja-build libX11-devel libXcursor-devel libXi-devel libXrandr-devel mesa-libGL-devel fontconfig-devel
On Arch:
sudo pacman -S gcc clang cmake ninja libx11 libxcursor mesa-libgl fontconfig libwebp
sudo pacman -S gcc clang cmake ninja libx11 libxcursor libxi libxrandr mesa-libgl fontconfig libwebp
On SUSE:
sudo zypper install gcc-c++ clang cmake ninja libX11-devel libXcursor-devel libXi-devel Mesa-libGL-devel fontconfig-devel
sudo zypper install gcc-c++ clang cmake ninja libX11-devel libXcursor-devel libXi-devel libXrandr-devel Mesa-libGL-devel fontconfig-devel
# Automatic Building

View File

@ -30,7 +30,7 @@
# HARFBUZZ_INCLUDE_DIRS - containg the HarfBuzz headers
# HARFBUZZ_LIBRARIES - containg the HarfBuzz library
include(FindPkgConfig)
find_package(PkgConfig QUIET)
pkg_check_modules(PC_HARFBUZZ harfbuzz>=0.9.7)

2
laf

@ -1 +1 @@
Subproject commit 3f1f86cc734443ba5c72d25c72cdf41ca208e9fe
Subproject commit 39706c11063fb53cf4c8e865102c6f71e2606906

View File

@ -220,6 +220,7 @@ if(ENABLE_TESTS)
find_tests(ui ui-lib)
find_tests(app/cli app-lib)
find_tests(app/file app-lib)
find_tests(app/ui app-lib)
find_tests(app app-lib)
find_tests(. app-lib)
endif()

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -15,6 +15,7 @@
#include "app/ui/editor/editor.h"
#include "os/system.h"
#include "os/window.h"
#include "ui/display.h"
namespace app {
@ -29,17 +30,15 @@ void initialize_color_spaces(Preferences& pref)
pref.color.manage.AfterChange.connect([](bool manage) { g_manage = manage; });
}
os::ColorSpaceRef get_screen_color_space()
os::ColorSpaceRef get_current_color_space(ui::Display* display, Doc* doc)
{
return os::System::instance()->defaultWindow()->colorSpace();
}
os::ColorSpaceRef get_current_color_space()
{
if (auto* editor = Editor::activeEditor())
return editor->document()->osColorSpace();
else
return get_screen_color_space();
if (!doc) {
if (auto* editor = Editor::activeEditor())
doc = editor->document();
}
if (doc)
return doc->osColorSpace();
return display->colorSpace();
}
gfx::ColorSpaceRef get_working_rgb_space_from_preferences()
@ -62,11 +61,11 @@ gfx::ColorSpaceRef get_working_rgb_space_from_preferences()
//////////////////////////////////////////////////////////////////////
// Color conversion
ConvertCS::ConvertCS()
ConvertCS::ConvertCS(ui::Display* display, Doc* doc)
{
if (g_manage) {
auto srcCS = get_current_color_space();
auto dstCS = get_screen_color_space();
auto srcCS = get_current_color_space(display, doc);
auto dstCS = display->colorSpace();
if (srcCS && dstCS)
m_conversion = os::System::instance()->convertBetweenColorSpace(srcCS, dstCS);
}
@ -95,9 +94,9 @@ gfx::Color ConvertCS::operator()(const gfx::Color c)
}
}
ConvertCS convert_from_current_to_screen_color_space()
ConvertCS convert_from_current_to_display_color_space(ui::Display* display)
{
return ConvertCS();
return ConvertCS(display);
}
ConvertCS convert_from_custom_to_srgb(const os::ColorSpaceRef& from)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2018-2020 Igara Studio S.A.
// Copyright (c) 2018-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -16,21 +16,25 @@ namespace doc {
class Sprite;
}
namespace ui {
class Display;
}
namespace app {
class Doc;
class Preferences;
void initialize_color_spaces(Preferences& pref);
os::ColorSpaceRef get_screen_color_space();
// Returns the color space of the current document.
os::ColorSpaceRef get_current_color_space();
os::ColorSpaceRef get_current_color_space(ui::Display* display, Doc* doc = nullptr);
gfx::ColorSpaceRef get_working_rgb_space_from_preferences();
class ConvertCS {
public:
ConvertCS();
ConvertCS() = delete;
ConvertCS(ui::Display* display, Doc* doc = nullptr);
ConvertCS(const os::ColorSpaceRef& srcCS, const os::ColorSpaceRef& dstCS);
ConvertCS(ConvertCS&&);
ConvertCS& operator=(const ConvertCS&) = delete;
@ -40,7 +44,7 @@ private:
os::Ref<os::ColorSpaceConversion> m_conversion;
};
ConvertCS convert_from_current_to_screen_color_space();
ConvertCS convert_from_current_to_display_color_space(ui::Display* display);
ConvertCS convert_from_custom_to_srgb(const os::ColorSpaceRef& from);
} // namespace app

View File

@ -225,7 +225,7 @@ private:
LockButtons lock(this);
// We need to create a copy of the shortcut because
// Key::disableShortcut() will modify the shortcuts() collection itself.
ui::Shortcut shortcut = m_key->shortcuts()[index];
Shortcut shortcut = m_key->shortcuts()[index];
if (ui::Alert::show(Strings::alerts_delete_shortcut(shortcut.toString())) != 1)
return;
@ -329,7 +329,7 @@ private:
gfx::Rect(keyXPos, y, contextXPos - keyXPos, dh * m_key->shortcuts().size()));
if (clip) {
int i = 0;
for (const Shortcut& shortcut : m_key->shortcuts()) {
for (const AppShortcut& shortcut : m_key->shortcuts()) {
if (i != m_hotShortcut || !m_changeButton) {
g->drawText(getShortcutText(shortcut), fg, bg, gfx::Point(keyXPos, y));
}
@ -362,7 +362,7 @@ private:
gfx::Rect bounds = this->bounds();
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
const Shortcuts* shortcuts = (m_key ? &m_key->shortcuts() : NULL);
const AppShortcuts* shortcuts = (m_key ? &m_key->shortcuts() : nullptr);
int y = bounds.y;
int dh = textSize().h + 4 * guiscale();
int maxi = (shortcuts && shortcuts->size() > 1 ? shortcuts->size() : 1);
@ -457,7 +457,7 @@ private:
m_hotShortcut = -1;
}
std::string getShortcutText(const Shortcut& shortcut) const
std::string getShortcutText(const AppShortcut& shortcut) const
{
if (m_key && m_key->type() == KeyType::WheelAction && shortcut.isEmpty()) {
return Strings::keyboard_shortcuts_default_action();

View File

@ -325,10 +325,15 @@ void FilterManagerImpl::applyToTarget()
void FilterManagerImpl::initTransaction()
{
ASSERT(!m_tx);
m_writer.reset(new ContextWriter(m_reader));
m_writer = std::make_unique<ContextWriter>(m_reader);
m_tx.reset(new Tx(*m_writer, m_filter->getName(), ModifyDocument));
}
void FilterManagerImpl::updateWriterThread()
{
document()->updateWriterThread();
}
bool FilterManagerImpl::isTransaction() const
{
return (m_tx != nullptr);

View File

@ -93,6 +93,7 @@ public:
void applyToTarget();
void initTransaction();
void updateWriterThread();
bool isTransaction() const;
void commitTransaction();

View File

@ -118,7 +118,13 @@ FilterWorker::~FilterWorker()
void FilterWorker::run()
{
// Initialize writting transaction
// Initialize writing transaction from the main thread. This is
// required to get the activeSite() from the UIContext from
// CmdTransaction::calcSpritePosition().
//
// The document will keep the UI thread associated as the "writer"
// thread, but that will be updated later in
// applyFilterInBackground() with the worker thread ID.
m_filterMgr->initTransaction();
std::thread thread;
@ -182,6 +188,11 @@ bool FilterWorker::isCancelled()
void FilterWorker::applyFilterInBackground()
{
try {
// This background thread is the new writer. This is required to
// avoid read-locking from the UI thread from Editor and Timeline
// onPaint() events.
m_filterMgr->updateWriterThread();
// Apply the filter
m_filterMgr->applyToTarget();

View File

@ -124,6 +124,11 @@ Doc::LockResult Doc::upgradeToWrite(int timeout)
return res;
}
void Doc::updateWriterThread()
{
m_rwLock.updateWriterThread();
}
void Doc::downgradeToRead(LockResult lockResult)
{
DOC_TRACE("DOC: downgradeToRead", this, (int)lockResult);

View File

@ -82,6 +82,7 @@ public:
LockResult readLock(int timeout);
LockResult writeLock(int timeout);
LockResult upgradeToWrite(int timeout);
void updateWriterThread();
void downgradeToRead(LockResult lockResult);
void unlock(LockResult lockResult);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2024 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -42,6 +42,7 @@
#include "archive_entry.h"
#include "json11.hpp"
#include <cctype>
#include <fstream>
#include <queue>
#include <sstream>
@ -535,6 +536,19 @@ void Extension::updateCategory(const Category newCategory)
#ifdef ENABLE_SCRIPTING
static bool is_simple_id(const char* k)
{
if (!*k)
return false;
if (!std::isalpha(*k) && *k != '_')
return false;
for (const char* p = k + 1; *p; ++p) {
if (!std::isalnum(*p) && *p != '_')
return false;
}
return true;
}
// TODO move this to app/script/tableutils.h
static void serialize_table(lua_State* L, int idx, std::string& result)
{
@ -555,7 +569,21 @@ static void serialize_table(lua_State* L, int idx, std::string& result)
// Save key
if (lua_type(L, -2) == LUA_TSTRING) {
if (const char* k = lua_tostring(L, -2)) {
result += k;
// If this is a simple identifier we can avoid using ["..."] to enclose
// the key name.
if (is_simple_id(k)) {
result += k;
}
// In other cases, we enclose the key name in brackets and
// double quotes ["..."] to avoid syntax errors when the name
// has unicode characters.
else {
result.push_back('[');
result.push_back('"');
result += k;
result.push_back('"');
result.push_back(']');
}
result.push_back('=');
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -30,6 +30,7 @@
#include "os/surface.h"
#include "os/system.h"
#include "ui/intern.h"
#include "ui/paint.h"
#include "ui/system.h"
#include "ui/theme.h"
@ -118,9 +119,6 @@ void draw_color(ui::Graphics* g,
app::Color color = _color;
const int alpha = color.getAlpha();
// Color space conversion
auto convertColor = convert_from_current_to_screen_color_space();
if (alpha < 255) {
if (rc.w == rc.h)
draw_checkered_grid(g, rc, gfx::Size(rc.w / 2, rc.h / 2));
@ -133,11 +131,15 @@ void draw_color(ui::Graphics* g,
color = app::Color::fromGray(color.getGray(), color.getAlpha());
}
// The color is in the current sprite color space.
ui::Paint paint;
paint.color(color_utils::color_for_ui(color), get_current_color_space(g->display()).get());
if (color.getType() == app::Color::IndexType) {
int index = color.getIndex();
if (index >= 0 && index < get_current_palette()->size()) {
g->fillRect(convertColor(color_utils::color_for_ui(color)), rc);
g->drawRect(rc, paint);
}
else {
g->fillRect(gfx::rgba(0, 0, 0), rc);
@ -147,7 +149,7 @@ void draw_color(ui::Graphics* g,
}
}
else {
g->fillRect(convertColor(color_utils::color_for_ui(color)), rc);
g->drawRect(rc, paint);
}
}
}
@ -219,7 +221,8 @@ void draw_tile(ui::Graphics* g, const Rect& rc, const Site& site, doc::tile_t ti
int w = tileImage->width();
int h = tileImage->height();
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(w, h);
os::SurfaceRef surface =
os::System::instance()->makeRgbaSurface(w, h, get_current_color_space(g->display()));
convert_image_to_surface(tileImage.get(), get_current_palette(), surface.get(), 0, 0, 0, 0, w, h);
ui::Paint paint;

View File

@ -406,7 +406,7 @@ void load_window_pos(Window* window, const char* section, const bool limitMinSiz
if (get_multiple_displays()) {
Rect frame = get_config_rect(section, "WindowFrame", gfx::Rect());
if (!frame.isEmpty()) {
limit_with_workarea(parentDisplay, frame);
limit_least(frame);
window->loadNativeFrame(frame);
}
}
@ -680,27 +680,13 @@ 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[] = { 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& k : *keys) {
if (k->isPressed(msg, contexts[i]) &&
(!key ||
(key->keycontext() != currentCtx && match_key_context(k->keycontext(), currentCtx)))) {
key = k;
}
}
}
const KeyPtr key = keys->findBestKeyFromMessage(msg);
if (!key)
return false;
// Cancel menu-bar loops (to close any popup menu)
App* app = App::instance();
app->mainWindow()->getMenuBar()->cancelMenuLoop();
switch (key->type()) {

View File

@ -10,6 +10,6 @@
// Increment this value if the scripting API is modified between two
// released Aseprite versions.
#define API_VERSION 35
#define API_VERSION 36
#endif

View File

@ -1,11 +1,12 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
// Copyright (c) 2022-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#include "app/script/canvas_widget.h"
#include "app/color_spaces.h"
#include "app/script/graphics_context.h"
#include "app/ui/skin/skin_theme.h"
#include "os/system.h"
@ -45,7 +46,7 @@ void Canvas::callPaint()
return;
os::Paint p;
p.color(bgColor());
p.color(bgColor(), m_surface->colorSpace().get());
m_surface->drawRect(m_surface->bounds(), p);
// Draw only on resize (onPaint we draw the cached m_surface)
@ -189,7 +190,9 @@ void Canvas::onResize(ui::ResizeEvent& ev)
}
if (!m_surface || m_surface->width() != w || m_surface->height() != h) {
m_surface = system->makeSurface(w, h);
ui::Display* display = this->display();
os::ColorSpaceRef cs = (display ? display->colorSpace() : nullptr);
m_surface = system->makeSurface(w, h, cs);
callPaint();
}
}

View File

@ -34,7 +34,6 @@ namespace app { namespace script {
template<>
void push_value_to_lua(lua_State* L, const std::nullptr_t&)
{
TRACEARGS("push_value_to_lua nullptr_t");
lua_pushnil(L);
}

View File

@ -10,6 +10,7 @@
#include "config.h"
#endif
#include "app/color_spaces.h"
#include "app/util/conversion_to_surface.h"
#include "doc/blend_mode.h"
#include "doc/cel.h"
@ -21,7 +22,8 @@
namespace app { namespace thumb {
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
os::SurfaceRef get_cel_thumbnail(ui::Display* display,
const doc::Cel* cel,
const bool scaleUpToFit,
const gfx::Size& fitInSize)
{
@ -55,7 +57,8 @@ os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
if (os::SurfaceRef thumbnail = os::System::instance()->makeRgbaSurface(
thumbnailImage->width(),
thumbnailImage->height())) {
thumbnailImage->height(),
get_current_color_space(display))) {
convert_image_to_surface(thumbnailImage.get(),
palette,
thumbnail.get(),

View File

@ -11,6 +11,7 @@
#include "gfx/size.h"
#include "os/surface.h"
#include "ui/display.h"
namespace doc {
class Cel;
@ -22,7 +23,8 @@ class Surface;
namespace app { namespace thumb {
os::SurfaceRef get_cel_thumbnail(const doc::Cel* cel,
os::SurfaceRef get_cel_thumbnail(ui::Display* display,
const doc::Cel* cel,
const bool scaleUpToFit,
const gfx::Size& fitInSize);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -107,11 +107,11 @@ public:
}
}
os::Surface* getCanvas(int w, int h, gfx::Color bgColor)
os::Surface* getCanvas(Display* display, int w, int h, gfx::Color bgColor)
{
assert_ui_thread();
auto activeCS = get_current_color_space();
auto activeCS = get_current_color_space(display);
if (!m_canvas || m_canvas->width() != w || m_canvas->height() != h ||
m_canvas->colorSpace() != activeCS) {
@ -432,9 +432,9 @@ void ColorSelector::onPaint(ui::PaintEvent& ev)
SkCanvas* canvas;
bool isSRGB;
// TODO compare both color spaces
if ((!get_current_color_space() || get_current_color_space()->isSRGB()) &&
(!g->getInternalSurface()->colorSpace() ||
g->getInternalSurface()->colorSpace()->isSRGB())) {
auto displayCs = get_current_color_space(display());
auto gCs = g->getInternalSurface()->colorSpace();
if ((!displayCs || displayCs->isSRGB()) && (!gCs || gCs->isSRGB())) {
// We can render directly in the ui::Graphics surface
canvas = &static_cast<os::SkiaSurface*>(g->getInternalSurface())->canvas();
isSRGB = true;
@ -442,7 +442,7 @@ void ColorSelector::onPaint(ui::PaintEvent& ev)
else {
// We'll paint in the ColorSelector::Painter canvas, and so we
// can convert color spaces.
painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
painterSurface = painter.getCanvas(display(), rc.w, rc.h, theme->colors.workspace());
canvas = &static_cast<os::SkiaSurface*>(painterSurface)->canvas();
isSRGB = false;
}
@ -497,7 +497,7 @@ void ColorSelector::onPaint(ui::PaintEvent& ev)
else
#endif // SK_ENABLE_SKSL
{
painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
painterSurface = painter.getCanvas(display(), rc.w, rc.h, theme->colors.workspace());
}
if (painterSurface)

View File

@ -58,9 +58,7 @@ public:
return;
}
// Color space conversion
auto convertColor = convert_from_current_to_screen_color_space();
Paint paint;
gfx::Color color = gfx::ColorNone;
int w = std::max(rc.w - 1, 1);
@ -110,7 +108,11 @@ public:
color = color_utils::color_for_ui(app::Color::fromGray(255 * x / w));
break;
}
g->drawVLine(convertColor(color), rc.x + x, rc.y, rc.h);
// Color space conversion
paint.color(color, get_current_color_space(slider->display()).get());
g->drawVLine(rc.x + x, rc.y, rc.h, paint);
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -11,6 +11,7 @@
#include "app/ui/color_wheel.h"
#include "app/color_spaces.h"
#include "app/color_utils.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
@ -358,6 +359,9 @@ void ColorWheel::onPaintMainArea(ui::Graphics* g, const gfx::Rect& rc)
int n = getHarmonies();
int boxsize = std::min(rc.w / 10, rc.h / 10);
ui::Paint paint;
auto cs = get_current_color_space(g->display());
for (int i = 0; i < n; ++i) {
app::Color color = getColorInHarmony(i);
double angle = color.getHsvHue() - 30.0;
@ -374,9 +378,10 @@ void ColorWheel::onPaintMainArea(ui::Graphics* g, const gfx::Rect& rc)
paintColorIndicator(g, pos, color.getHsvValue() < 0.5);
g->fillRect(
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), 255),
gfx::Rect(rc.x + rc.w - (n - i) * boxsize, rc.y + rc.h - boxsize, boxsize, boxsize));
paint.color(gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), 255), cs.get());
g->drawRect(
gfx::Rect(rc.x + rc.w - (n - i) * boxsize, rc.y + rc.h - boxsize, boxsize, boxsize),
paint);
}
}
}

View File

@ -2763,7 +2763,7 @@ void Editor::pasteImage(const Image* image, const Mask* mask, const gfx::Point*
ASSERT(image);
std::unique_ptr<Mask> temp_mask;
if (!mask) {
if (!mask || mask->bounds().isEmpty()) {
gfx::Rect visibleBounds = getVisibleSpriteBounds();
gfx::Rect imageBounds = image->bounds();

View File

@ -202,27 +202,25 @@ std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelActio
return {};
}
void erase_shortcut(app::KeySourceShortcutList& kvs,
void erase_shortcut(app::AppShortcuts& kvs,
const app::KeySource source,
const ui::Shortcut& shortcut)
{
for (auto it = kvs.begin(); it != kvs.end();) {
auto& kv = *it;
if (kv.first == source && kv.second == shortcut) {
if (kv.source() == source && kv.shortcut() == shortcut)
it = kvs.erase(it);
}
else
++it;
}
}
void erase_shortcuts(app::KeySourceShortcutList& kvs, const app::KeySource source)
void erase_shortcuts(app::AppShortcuts& kvs, const app::KeySource source)
{
for (auto it = kvs.begin(); it != kvs.end();) {
auto& kv = *it;
if (kv.first == source) {
if (kv.source() == source)
it = kvs.erase(it);
}
else
++it;
}
@ -297,6 +295,32 @@ namespace app {
using namespace ui;
bool AppShortcut::fitsBetterThan(const KeyContext currentContext,
const KeyContext thisShortcutContext,
const KeyContext otherShortcutContext,
const AppShortcut& otherShortcut) const
{
// Better context in the same source
if (otherShortcut.source() == this->source() && otherShortcutContext != currentContext &&
thisShortcutContext == currentContext)
return true;
// Better key source/level: User-defined > Extension-defined > App-defined
if (int(source()) > int(otherShortcut.source()) && (thisShortcutContext == currentContext ||
// User-defined "Any" context overwrites all
// app-defined context
thisShortcutContext == KeyContext::Any))
return true;
// Normal > SelectionTool > Transformation
if ((currentContext == KeyContext::Transformation &&
otherShortcutContext != KeyContext::Transformation &&
thisShortcutContext == KeyContext::SelectionTool))
return true;
return false;
}
Key::Key(const Key& k)
: m_type(k.m_type)
, m_adds(k.m_adds)
@ -389,38 +413,38 @@ KeyPtr Key::MakeDragAction(WheelAction dragAction)
return k;
}
const ui::Shortcuts& Key::shortcuts() const
const AppShortcuts& Key::shortcuts() const
{
if (!m_shortcuts) {
m_shortcuts = std::make_unique<ui::Shortcuts>();
m_shortcuts = std::make_unique<AppShortcuts>();
// Add default keys
for (const auto& kv : m_adds) {
if (kv.first == KeySource::Original)
m_shortcuts->add(kv.second);
if (kv.source() == KeySource::Original)
m_shortcuts->add(kv);
}
// Delete/add extension-defined keys
for (const auto& kv : m_dels) {
if (kv.first == KeySource::ExtensionDefined)
m_shortcuts->remove(kv.second);
if (kv.source() == KeySource::ExtensionDefined)
m_shortcuts->remove(kv);
else {
ASSERT(kv.first != KeySource::Original);
ASSERT(kv.source() != KeySource::Original);
}
}
for (const auto& kv : m_adds) {
if (kv.first == KeySource::ExtensionDefined)
m_shortcuts->add(kv.second);
if (kv.source() == KeySource::ExtensionDefined)
m_shortcuts->add(kv);
}
// Delete/add user-defined keys
for (const auto& kv : m_dels) {
if (kv.first == KeySource::UserDefined)
m_shortcuts->remove(kv.second);
if (kv.source() == KeySource::UserDefined)
m_shortcuts->remove(kv);
}
for (const auto& kv : m_adds) {
if (kv.first == KeySource::UserDefined)
m_shortcuts->add(kv.second);
if (kv.source() == KeySource::UserDefined)
m_shortcuts->add(kv);
}
}
return *m_shortcuts;
@ -428,7 +452,7 @@ const ui::Shortcuts& Key::shortcuts() const
void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShortcuts& globalKeys)
{
m_adds.emplace_back(source, shortcut);
m_adds.push_back(AppShortcut(source, shortcut));
m_shortcuts.reset();
// Remove the shortcut from other commands
@ -439,32 +463,59 @@ void Key::add(const ui::Shortcut& shortcut, const KeySource source, KeyboardShor
}
}
const ui::Shortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
bool Key::fitsContext(const KeyContext keyContext) const
{
// This key is for any context
if (m_keycontext == KeyContext::Any)
return true;
// This key is for the same context
if (m_keycontext == keyContext)
return true;
// Use Normal or SelectionTool keys in Transformation context
if (keyContext == KeyContext::Transformation &&
(m_keycontext == KeyContext::SelectionTool || m_keycontext == KeyContext::Normal))
return true;
// Use Normal keys in SelectionTool or FramesSelection contexts
if ((keyContext == KeyContext::SelectionTool || keyContext == KeyContext::FramesSelection) &&
(m_keycontext == KeyContext::Normal))
return true;
return false;
}
const AppShortcut* Key::isPressed(const Message* msg, const KeyContext keyContext) const
{
const AppShortcut* best = nullptr;
if (const auto* keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
for (const Shortcut& shortcut : shortcuts()) {
if (shortcut.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
(m_keycontext == KeyContext::Any || match_key_context(m_keycontext, keyContext))) {
return &shortcut;
if (fitsContext(keyContext)) {
for (const AppShortcut& shortcut : shortcuts()) {
if (shortcut.isPressed(keyMsg->modifiers(), keyMsg->scancode(), keyMsg->unicodeChar()) &&
(!best || shortcut.fitsBetterThan(keyContext, keycontext(), keycontext(), *best))) {
best = &shortcut;
}
}
}
}
else if (const auto* mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
for (const Shortcut& shortcut : shortcuts()) {
if ((shortcut.modifiers() == mouseMsg->modifiers()) &&
(m_keycontext == KeyContext::Any ||
// TODO we could have multiple mouse wheel key-context,
// like "sprite editor" context, or "timeline" context,
// etc.
m_keycontext == KeyContext::MouseWheel)) {
return &shortcut;
if (m_keycontext == KeyContext::Any ||
// TODO we could have multiple mouse wheel key-context,
// like "sprite editor" context, or "timeline" context,
// etc.
m_keycontext == KeyContext::MouseWheel) {
for (const AppShortcut& shortcut : shortcuts()) {
if (shortcut.modifiers() == mouseMsg->modifiers())
return &shortcut;
}
}
}
return nullptr;
return best;
}
const ui::Shortcut* Key::isPressed(const Message* msg) const
const AppShortcut* Key::isPressed(const Message* msg) const
{
return isPressed(msg, KeyboardShortcuts::getCurrentKeyContext());
}
@ -472,7 +523,7 @@ const ui::Shortcut* Key::isPressed(const Message* msg) const
bool Key::isPressed() const
{
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
return std::any_of(ss.begin(), ss.end(), [](const AppShortcut& shortcut) {
return shortcut.isPressed();
});
}
@ -480,7 +531,7 @@ bool Key::isPressed() const
bool Key::isLooselyPressed() const
{
const auto& ss = this->shortcuts();
return std::any_of(ss.begin(), ss.end(), [](const Shortcut& shortcut) {
return std::any_of(ss.begin(), ss.end(), [](const AppShortcut& shortcut) {
return shortcut.isLooselyPressed();
});
}
@ -492,13 +543,16 @@ bool Key::isCommandListed() const
bool Key::hasShortcut(const ui::Shortcut& shortcut) const
{
return shortcuts().has(shortcut);
return shortcuts().has(AppShortcut(
// KeySource is not used in has()
KeySource::Original,
shortcut));
}
bool Key::hasUserDefinedShortcuts() const
{
return std::any_of(m_adds.begin(), m_adds.end(), [](const auto& kv) {
return (kv.first == KeySource::UserDefined);
return (kv.source() == KeySource::UserDefined);
});
}
@ -511,7 +565,7 @@ void Key::disableShortcut(const ui::Shortcut& shortcut, const KeySource source)
erase_shortcut(m_adds, source, shortcut);
erase_shortcut(m_dels, source, shortcut);
m_dels.emplace_back(source, shortcut);
m_dels.push_back(AppShortcut(source, shortcut));
m_shortcuts.reset();
}
@ -531,7 +585,7 @@ void Key::copyOriginalToUser()
// Then copy all original & extension-defined keys as user-defined
auto copy = m_adds;
for (const auto& kv : copy)
m_adds.emplace_back(KeySource::UserDefined, kv.second);
m_adds.push_back(AppShortcut(KeySource::UserDefined, kv));
m_shortcuts.reset();
}

View File

@ -103,12 +103,42 @@ inline KeyAction operator&(KeyAction a, KeyAction b)
return KeyAction(int(a) & int(b));
}
// This is a ui::Shortcut wrapper (just one key shortcut) defined by
// the app, an extension, or the user (KeySource).
class AppShortcut : public ui::Shortcut {
public:
AppShortcut(const KeySource source, const ui::Shortcut& shortcut)
: Shortcut(shortcut)
, m_source(source)
{
}
KeySource source() const { return m_source; }
const ui::Shortcut& shortcut() const { return *this; }
// bool operator==(const AppShortcut& other) const { return shortcut.operator==(other.shortcut); }
// bool operator!=(const AppShortcut& other) const { return shortcut.operator!=(other.shortcut); }
// Returns true if this AppShortcut is better for the current
// context, compared to another shortcut.
bool fitsBetterThan(KeyContext currentContext,
KeyContext thisShortcutContext,
KeyContext otherShortcutContext,
const AppShortcut& otherShortcut) const;
private:
KeySource m_source;
};
using AppShortcuts = ui::ShortcutsT<AppShortcut>;
class Key;
using KeyPtr = std::shared_ptr<Key>;
using Keys = std::vector<KeyPtr>;
using KeySourceShortcutList = std::vector<std::pair<KeySource, ui::Shortcut>>;
using DragVector = base::Vector2d<double>;
// A set of key shortcuts (AppShortcuts) associated to one command,
// tool, or specific action.
class Key {
public:
Key(const Key& key);
@ -119,13 +149,15 @@ public:
static KeyPtr MakeDragAction(WheelAction dragAction);
KeyType type() const { return m_type; }
const ui::Shortcuts& shortcuts() const;
const KeySourceShortcutList& addsKeys() const { return m_adds; }
const KeySourceShortcutList& delsKeys() const { return m_dels; }
const AppShortcuts& shortcuts() const;
const AppShortcuts& addsKeys() const { return m_adds; }
const AppShortcuts& delsKeys() const { return m_dels; }
void add(const ui::Shortcut& shortcut, KeySource source, KeyboardShortcuts& globalKeys);
const ui::Shortcut* isPressed(const ui::Message* msg, KeyContext keyContext) const;
const ui::Shortcut* isPressed(const ui::Message* msg) const;
bool fitsContext(KeyContext keyContext) const;
const AppShortcut* isPressed(const ui::Message* msg, KeyContext keyContext) const;
const AppShortcut* isPressed(const ui::Message* msg) const;
bool isPressed() const;
bool isLooselyPressed() const;
bool isCommandListed() const;
@ -161,11 +193,11 @@ public:
private:
KeyType m_type;
KeySourceShortcutList m_adds;
KeySourceShortcutList m_dels;
AppShortcuts m_adds;
AppShortcuts m_dels;
// Final list of shortcuts after processing the
// addition/deletion of extension-defined & user-defined keys.
mutable std::unique_ptr<ui::Shortcuts> m_shortcuts;
mutable std::unique_ptr<AppShortcuts> m_shortcuts;
KeyContext m_keycontext;
// for KeyType::Command

View File

@ -11,28 +11,61 @@
namespace app {
// Specifies a possible App state/context where the user can press a
// key and associate different actions/commands in that specific
// context. Useful to associate the same key to different actions
// depending on the current context.
//
// This list should be sorted in such a way that more specific
// contexts are below:
//
// Normal > SelectionTool > Transformation
//
// This means that keys defined in SelectionTool are still valid in
// Transformation context.
//
// Other key context are just temporary in the Editor, like a mouse
// drag operation when we are transforming the
// selection. E.g. TranslatingSelection, ScalingSelection, etc.
enum class KeyContext {
// Special context to define keys. This is not used as a "current
// key context", but just to define keys for the whole range of key
// contexts.
Any,
// Regular context in a standby user state, the user is not dragging
// or clicking the mouse.
Normal,
// The user has a selection on the canvas and a selection-like tool
// (selection ink) selected.
SelectionTool,
// Special context to modify a specific action when the mouse is
// above the handles to translate/scale/rotate or is already
// translating/scaling/rotating the selection.
TranslatingSelection,
ScalingSelection,
RotatingSelection,
// Special context when the user has a specific tool selected to
// modify the tool behavior with a key:
MoveTool,
FreehandTool,
ShapeTool,
// When the mouse wheel is used above the editor.
MouseWheel,
// The user has a range of frames/cels selected in the timeline.
FramesSelection,
// The user has moved the selection and has yet to drop the pixels
// (the user can drop or undo the transformation).
Transformation,
};
inline bool match_key_context(const KeyContext a, const KeyContext b)
{
return (a == b) || (a == KeyContext::Any || b == KeyContext::Any) ||
((a == KeyContext::SelectionTool && b == KeyContext::Transformation) ||
(a == KeyContext::Transformation && b == KeyContext::SelectionTool));
}
} // namespace app
#endif

View File

@ -89,8 +89,10 @@ void KeyboardShortcuts::destroyInstance()
KeyboardShortcuts::KeyboardShortcuts()
{
ASSERT(Strings::instance());
Strings::instance()->LanguageChange.connect([] { reset_key_tables_that_depends_on_language(); });
// Strings instance can be nullptr in tests.
if (auto* strings = Strings::instance()) {
strings->LanguageChange.connect([] { reset_key_tables_that_depends_on_language(); });
}
}
KeyboardShortcuts::~KeyboardShortcuts()
@ -367,12 +369,12 @@ void KeyboardShortcuts::exportKeys(XMLElement* parent, KeyType type)
continue;
for (const auto& kv : key->delsKeys())
if (kv.first == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv.second, true);
if (kv.source() == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv, true);
for (const auto& kv : key->addsKeys())
if (kv.first == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv.second, false);
if (kv.source() == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv, false);
}
}
@ -574,23 +576,47 @@ KeyContext KeyboardShortcuts::getCurrentKeyContext()
return KeyContext::Normal;
}
bool KeyboardShortcuts::getCommandFromKeyMessage(const Message* msg,
Command** command,
Params* params)
KeyPtr KeyboardShortcuts::findBestKeyFromMessage(const ui::Message* msg,
KeyContext currentKeyContext,
std::optional<KeyType> filterByType) const
{
const KeyContext contexts[] = { getCurrentKeyContext(), KeyContext::Normal };
const KeyContext contexts[] = { currentKeyContext, KeyContext::Normal };
int n = (contexts[0] != contexts[1] ? 2 : 1);
KeyPtr bestKey = nullptr;
const AppShortcut* bestShortcut = nullptr;
for (int i = 0; i < n; ++i) {
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Command && key->isPressed(msg, contexts[i])) {
if (command)
*command = key->command();
if (params)
*params = key->params();
return true;
for (const KeyPtr& key : m_keys) {
// Skip keys that are not for the specific KeyType (e.g. only for commands).
if (filterByType.has_value() && key->type() != *filterByType)
continue;
const AppShortcut* shortcut = key->isPressed(msg, contexts[i]);
if (shortcut && (!bestKey || shortcut->fitsBetterThan(currentKeyContext,
key->keycontext(),
bestKey->keycontext(),
*bestShortcut))) {
bestKey = key;
bestShortcut = shortcut;
}
}
}
return bestKey;
}
bool KeyboardShortcuts::getCommandFromKeyMessage(const ui::Message* msg,
Command** command,
Params* params,
KeyContext currentKeyContext)
{
KeyPtr key = findBestKeyFromMessage(msg, currentKeyContext, std::make_optional(KeyType::Command));
if (key) {
ASSERT(key->type() == KeyType::Command);
if (command)
*command = key->command();
if (params)
*params = key->params();
return true;
}
return false;
}
@ -634,10 +660,10 @@ WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext c
const ui::Message* msg)
{
WheelAction wheelAction = WheelAction::None;
const ui::Shortcut* bestShortcut = nullptr;
const AppShortcut* bestShortcut = nullptr;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::WheelAction && key->keycontext() == context) {
const ui::Shortcut* shortcut = key->isPressed(msg);
const AppShortcut* shortcut = key->isPressed(msg);
if ((shortcut) && (!bestShortcut || bestShortcut->modifiers() < shortcut->modifiers())) {
bestShortcut = shortcut;
wheelAction = key->wheelAction();
@ -653,7 +679,7 @@ Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const ui::Message* msg)
Keys keys;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::DragAction) {
const ui::Shortcut* shortcut = key->isPressed(msg);
const AppShortcut* shortcut = key->isPressed(msg);
if (shortcut) {
keys.push_back(key);
}

View File

@ -12,6 +12,8 @@
#include "app/ui/key.h"
#include "obs/signal.h"
#include <optional>
namespace tinyxml2 {
class XMLElement;
}
@ -59,7 +61,18 @@ public:
const Key* newKey);
static KeyContext getCurrentKeyContext();
bool getCommandFromKeyMessage(const ui::Message* msg, Command** command, Params* params);
KeyPtr findBestKeyFromMessage(
const ui::Message* msg,
KeyContext currentKeyContext = KeyboardShortcuts::getCurrentKeyContext(),
std::optional<KeyType> filterByType = std::nullopt) const;
bool getCommandFromKeyMessage(
const ui::Message* msg,
Command** command,
Params* params,
KeyContext currentKeyContext = KeyboardShortcuts::getCurrentKeyContext());
tools::Tool* getCurrentQuicktool(tools::Tool* currentTool);
KeyAction getCurrentActionModifiers(KeyContext context);
WheelAction getWheelActionFromMouseMessage(KeyContext context, const ui::Message* msg);

View File

@ -0,0 +1,184 @@
// 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 <gtest/gtest.h>
#include "app/ui/keyboard_shortcuts.h"
#include "app/app.h"
#include "app/cli/app_options.h"
#include "app/commands/command.h"
#include "app/commands/command_ids.h"
#include "os/system.h"
#include "ui/app_state.h"
#include "ui/message.h"
using namespace app;
using namespace ui;
static KeyboardShortcuts* ks = nullptr;
#define DEFINE_KEY(commandId, scancode, keycontext) \
{ \
KeyPtr k = ks->command(CommandId::commandId(), {}, keycontext); \
k->add(ui::Shortcut(kKeyNoneModifier, scancode, 0), KeySource::Original, *ks); \
}
#define DEFINE_USER_KEY(commandId, scancode, keycontext) \
{ \
KeyPtr k = ks->command(CommandId::commandId(), {}, keycontext); \
k->add(ui::Shortcut(kKeyNoneModifier, scancode, 0), KeySource::UserDefined, *ks); \
}
#define EXPECT_COMMAND_FOR_KEY(commandId, scancode, keycontext) \
{ \
KeyMessage msg(kKeyDownMessage, scancode, kKeyNoneModifier, 0, 0); \
Command* cmd = nullptr; \
EXPECT_TRUE(ks->getCommandFromKeyMessage(&msg, &cmd, nullptr, keycontext)); \
ASSERT_TRUE(cmd != nullptr) << "command not found for key"; \
EXPECT_EQ(CommandId::commandId(), cmd->id()) << "other command found: " << cmd->id(); \
}
#define NO_COMMAND_FOR_KEY(scancode, keycontext) \
{ \
KeyMessage msg(kKeyDownMessage, scancode, kKeyNoneModifier, 0, 0); \
Command* cmd = nullptr; \
EXPECT_FALSE(ks->getCommandFromKeyMessage(&msg, &cmd, nullptr, keycontext)); \
ASSERT_TRUE(cmd == nullptr) << "command found for key: " << cmd->id(); \
}
TEST(KeyboardShortcuts, Basic)
{
ks->clear();
KeyPtr k = ks->command(CommandId::Undo(), {}, KeyContext::Any);
// Simulate a key press and check that the command is not yet
// associated to the 'Z' key.
{
KeyMessage msg(kKeyDownMessage, kKeyZ, kKeyNoneModifier, 0, 0);
Command* cmd = nullptr;
Params params;
EXPECT_FALSE(ks->getCommandFromKeyMessage(&msg, &cmd, &params));
}
// Associate the command to the 'Z' key and check.
k->add(ui::Shortcut(kKeyNoneModifier, kKeyZ, 0), KeySource::Original, *ks);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyZ, KeyContext::Normal);
}
TEST(KeyboardShortcuts, KeyContexts)
{
ks->clear();
DEFINE_KEY(Cancel, kKeyEsc, KeyContext::Any);
DEFINE_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
DEFINE_KEY(PlayAnimation, kKeyEnter, KeyContext::Normal);
DEFINE_KEY(Clear, kKeyBackspace, KeyContext::Normal);
DEFINE_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
DEFINE_KEY(Apply, kKeyEnter, KeyContext::Transformation);
DEFINE_KEY(Cut, kKeyX, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Cancel, kKeyEsc, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(PlayAnimation, kKeyEnter, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(PlayAnimation, kKeyEnter, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Apply, kKeyEnter, KeyContext::Transformation);
EXPECT_COMMAND_FOR_KEY(Clear, kKeyBackspace, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Clear, kKeyBackspace, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Clear, kKeyBackspace, KeyContext::Transformation);
NO_COMMAND_FOR_KEY(kKeyX, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Cut, kKeyX, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Cut, kKeyX, KeyContext::Transformation);
}
TEST(KeyboardShortcuts, UserDefinedPriority)
{
ks->clear();
DEFINE_KEY(Undo, kKeyZ, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyZ, KeyContext::Normal);
DEFINE_USER_KEY(Redo, kKeyZ, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Redo, kKeyZ, KeyContext::Normal);
DEFINE_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
DEFINE_USER_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
DEFINE_USER_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Any);
EXPECT_COMMAND_FOR_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::SelectionTool);
}
TEST(KeyboardShortcuts, SpecificContextHasMorePriorityButNotIfItsUserDefined)
{
ks->clear();
DEFINE_KEY(Cancel, kKeyEsc, KeyContext::Any);
DEFINE_KEY(Undo, kKeyEsc, KeyContext::Transformation);
// Pressing "Esc" in "Transformation" context should run "Undo",
// although "Cancel" is defined for "Any" context, a more specific
// context should have more priority.
EXPECT_COMMAND_FOR_KEY(Undo, kKeyEsc, KeyContext::Transformation);
// But an user-defined key, even for Any context, will overwrite the
// app-defined shortcut in all contexts.
DEFINE_USER_KEY(Zoom, kKeyEsc, KeyContext::Any);
EXPECT_COMMAND_FOR_KEY(Zoom, kKeyEsc, KeyContext::Transformation);
}
// Test that we can configure the Left key to always Undo when the
// default configuration says that the Left key does other actions in
// different contexts.
//
// Related issue: https://github.com/aseprite/aseprite/issues/5390
TEST(KeyboardShortcuts, UndoWithLeftAndRight)
{
ks->clear();
DEFINE_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
DEFINE_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(GotoPreviousFrame, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::SelectionTool);
// "Transformation" is a sub-context of "Selection" context
EXPECT_COMMAND_FOR_KEY(MoveMask, kKeyLeft, KeyContext::Transformation);
// Now we try defining the "Left" key for "Any" context overwriting it in all contexts.
DEFINE_USER_KEY(Undo, kKeyLeft, KeyContext::Any);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyLeft, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyLeft, KeyContext::SelectionTool);
EXPECT_COMMAND_FOR_KEY(Undo, kKeyLeft, KeyContext::Transformation);
}
TEST(KeyboardShortcuts, FramesSelection)
{
ks->clear();
DEFINE_KEY(LayerProperties, kKeyF2, KeyContext::Normal);
DEFINE_KEY(SetLoopSection, kKeyF2, KeyContext::FramesSelection);
EXPECT_COMMAND_FOR_KEY(LayerProperties, kKeyF2, KeyContext::Normal);
EXPECT_COMMAND_FOR_KEY(SetLoopSection, kKeyF2, KeyContext::FramesSelection);
}
int app_main(int argc, char* argv[])
{
os::SystemRef system = os::System::make();
const char* argv2[] = { argv[0] };
app::AppOptions options(sizeof(argv2) / sizeof(argv2[0]), argv2);
app::App app;
app.initialize(options);
app.run(false);
ks = KeyboardShortcuts::instance();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -13,6 +13,7 @@
#include "app/app.h"
#include "app/color.h"
#include "app/color_spaces.h"
#include "app/color_utils.h"
#include "app/commands/commands.h"
#include "app/modules/gfx.h"
@ -307,7 +308,8 @@ public:
if (tileImage) {
int w = tileImage->width();
int h = tileImage->height();
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(w, h);
os::SurfaceRef surface =
os::System::instance()->makeRgbaSurface(w, h, get_current_color_space(g->display()));
convert_image_to_surface(tileImage.get(),
get_current_palette(),
surface.get(),

View File

@ -11,6 +11,7 @@
#include "app/ui/tabs.h"
#include "app/color_spaces.h"
#include "app/color_utils.h"
#include "app/modules/gfx.h"
#include "app/modules/gui.h"
@ -969,7 +970,10 @@ void Tabs::createFloatingUILayer(Tab* tab)
ASSERT(!m_floatingUILayer);
ui::Display* display = this->display();
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(tab->width, m_tabsHeight);
os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(
tab->width,
m_tabsHeight,
get_current_color_space(display));
// Fill the surface with pink color
{

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -50,10 +50,8 @@ TaskWidget::TaskWidget(const Type type, base::task::func_t&& func)
}
else if (m_progressBar.parent()) {
float v = m_task.progress();
if (v > 0.0f) {
TRACEARGS("progressBar setValue", int(std::clamp(v * 100.0f, 0.0f, 100.0f)));
if (v > 0.0f)
m_progressBar.setValue(int(std::clamp(v * 100.0f, 0.0f, 100.0f)));
}
}
});
m_monitorTimer.start();

View File

@ -2526,7 +2526,7 @@ void Timeline::drawCel(ui::Graphics* g,
if (!thumb_bounds.isEmpty()) {
if (os::SurfaceRef surface =
thumb::get_cel_thumbnail(cel, m_scaleUpToFit, thumb_bounds.size())) {
thumb::get_cel_thumbnail(g->display(), cel, m_scaleUpToFit, thumb_bounds.size())) {
const int t = std::clamp(thumb_bounds.w / 8, 4, 16);
draw_checkered_grid(g, thumb_bounds, gfx::Size(t, t), docPref());
@ -2618,7 +2618,8 @@ void Timeline::drawCelOverlay(ui::Graphics* g)
return;
gfx::Rect rc = m_sprite->bounds().fitIn(gfx::Rect(m_thumbnailsOverlayBounds).shrink(1));
if (os::SurfaceRef surface = thumb::get_cel_thumbnail(cel, m_scaleUpToFit, rc.size())) {
if (os::SurfaceRef surface =
thumb::get_cel_thumbnail(g->display(), cel, m_scaleUpToFit, rc.size())) {
draw_checkered_grid(g, rc, gfx::Size(8, 8) * ui::guiscale(), docPref());
g->drawRgbaSurface(surface.get(),
@ -4630,13 +4631,13 @@ void Timeline::onDrop(ui::DragEvent& e)
std::string txmsg;
std::unique_ptr<docapi::DocProvider> docProvider = nullptr;
if (droppedImage) {
txmsg = "Dropped image on timeline";
txmsg = "Drop Image";
doc::ImageRef image = nullptr;
convert_surface_to_image(surface.get(), 0, 0, surface->width(), surface->height(), image);
docProvider = std::make_unique<DocProviderFromImage>(image);
}
else {
txmsg = "Dropped paths on timeline";
txmsg = "Drop File";
docProvider = std::make_unique<DocProviderFromPaths>(m_document->context(), paths);
}

View File

@ -25,6 +25,7 @@
#include "app/ui/workspace.h"
#include "app/ui/workspace_tabs.h"
#include "doc/sprite.h"
#include "ui/manager.h"
#include "ui/system.h"
#include <algorithm>
@ -120,6 +121,10 @@ void UIContext::setActiveView(DocView* docView)
mainWin->getTimeline()->updateUsingEditor(editor);
mainWin->getPreviewEditor()->updateUsingEditor(editor);
// Update mouse widgets immediately after changing views rather
// than waiting for mouse movement.
mainWin->manager()->_updateMouseWidgets();
// Change the image-type of color bar.
ColorBar::instance()->setPixelFormat(app_get_current_pixel_format());

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2024 Igara Studio S.A.
// Copyright (c) 2024-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -26,7 +26,7 @@ uint32_t convert_color_to_image(gfx::Color c, const os::SurfaceFormatData* fd)
uint8_t b = ((c & fd->blueMask) >> fd->blueShift);
uint8_t a = ((c & fd->alphaMask) >> fd->alphaShift);
if (fd->pixelAlpha == os::PixelAlpha::kPremultiplied) {
if (a > 0 && fd->pixelAlpha == os::PixelAlpha::kPremultiplied) {
r = r * 255 / a;
g = g * 255 / a;
b = b * 255 / a;

View File

@ -9,11 +9,11 @@ if [ $# -ge 2 -a $# -lt 4 ]; then
mkdir -p /tmp/Aseprite
filename=${1//\//.}$RANDOM
if [ $# -eq 2 ]; then
aseprite -b --frame-range "0,0" $1 --sheet /tmp/Aseprite/$filename.png
aseprite -b --frame-range "0,0" "$1" --sheet "/tmp/Aseprite/$filename.png"
elif [ $# -eq 3 ]; then
aseprite -b --frame-range "0,0" $1 --shrink-to "$3,$3" --sheet /tmp/Aseprite/$filename.png
aseprite -b --frame-range "0,0" "$1" --shrink-to "$3,$3" --sheet "/tmp/Aseprite/$filename.png"
fi
mkdir -p $(dirname "$2"); mv /tmp/Aseprite/$filename.png $2;
mkdir -p "$(dirname "$2")"; mv "/tmp/Aseprite/$filename.png" "$2";
else
echo "Parameters for aseprite thumbnailer are: inputfile outputfile [size]"
fi

View File

@ -1,5 +1,5 @@
// Aseprite Steam Wrapper
// Copyright (c) 2020-2024 Igara Studio S.A.
// Copyright (c) 2020-2025 Igara Studio S.A.
// Copyright (c) 2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -153,8 +153,6 @@ public:
CallbackMsg_t msg;
if (SteamAPI_ManualDispatch_GetNextCallback(m_pipe, &msg)) {
// TRACEARGS("SteamAPI_ManualDispatch_GetNextCallback", msg.callback);
bool disconnected = false;
switch (msg.callback) {
case kSteamServersDisconnected:

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -31,6 +31,7 @@ public:
Display* parentDisplay() { return m_parentDisplay; }
os::Window* nativeWindow() const { return m_nativeWindow.get(); }
os::SurfaceRef nativeSurface() const;
os::ColorSpaceRef colorSpace() const { return m_nativeWindow->colorSpace(); }
UILayers layers() { return m_layers; }
UILayerRef backLayer() { return m_layers.front(); }

View File

@ -20,21 +20,20 @@
#include "ui/window.h"
#include <algorithm>
#include <climits>
namespace ui {
#if 0 // TODO unused function, referenced in a comment in this file
static gfx::Region get_workarea_region()
// if the number of monitors changes during runtime this could break
static const std::vector<gfx::Rect> get_all_workareas()
{
// Returns the union of the workarea of all available screens
gfx::Region wa;
std::vector<gfx::Rect> workareas;
os::ScreenList screens;
os::System::instance()->listScreens(screens);
for (const auto& screen : screens)
wa |= gfx::Region(screen->workarea());
return wa;
workareas.push_back(screen->workarea());
return workareas;
}
#endif
int fit_bounds(Display* display, int arrowAlign, const gfx::Rect& target, gfx::Rect& bounds)
{
@ -120,7 +119,7 @@ void fit_bounds(const Display* parentDisplay,
if (get_multiple_displays() && window->shouldCreateNativeWindow()) {
const os::Window* nativeWindow = const_cast<ui::Display*>(parentDisplay)->nativeWindow();
// Limit to the current screen workarea (instead of using all the
// available workarea between all monitors, get_workarea_region())
// available workarea between all monitors)
const gfx::Rect workarea = nativeWindow->screen()->workarea();
const int scale = nativeWindow->scale();
@ -158,38 +157,63 @@ void fit_bounds(const Display* parentDisplay,
}
}
// Limit window position using the union of all workareas
//
// TODO at least the title bar should be visible so we can
// resize it, because workareas can form an irregular shape
// (not rectangular) the calculation is a little more
// complex
void limit_with_workarea(Display* parentDisplay, gfx::Rect& frame)
void limit_with_workarea(const gfx::Rect& workareaBounds, gfx::Rect& frame)
{
if (!get_multiple_displays())
return;
ASSERT(parentDisplay);
gfx::Rect waBounds = parentDisplay->nativeWindow()->screen()->workarea();
if (frame.x < waBounds.x)
frame.x = waBounds.x;
if (frame.y < waBounds.y)
frame.y = waBounds.y;
if (frame.x2() > waBounds.x2()) {
frame.x -= frame.x2() - waBounds.x2();
if (frame.x < waBounds.x) {
frame.x = waBounds.x;
frame.w = waBounds.w;
if (frame.x < workareaBounds.x)
frame.x = workareaBounds.x;
if (frame.y < workareaBounds.y)
frame.y = workareaBounds.y;
if (frame.x2() > workareaBounds.x2()) {
frame.x -= frame.x2() - workareaBounds.x2();
if (frame.x < workareaBounds.x) {
frame.x = workareaBounds.x;
frame.w = workareaBounds.w;
}
}
if (frame.y2() > waBounds.y2()) {
frame.y -= frame.y2() - waBounds.y2();
if (frame.y < waBounds.y) {
frame.y = waBounds.y;
frame.h = waBounds.h;
if (frame.y2() > workareaBounds.y2()) {
frame.y -= frame.y2() - workareaBounds.y2();
if (frame.y < workareaBounds.y) {
frame.y = workareaBounds.y;
frame.h = workareaBounds.h;
}
}
}
// since sqrt(a) < sqrt(b) for all a < b, we only have to square to compare
inline int distance_squared(const gfx::Rect& r1, const gfx::Rect& r2)
{
int dx = r1.x - r2.x;
int dy = r1.y - r2.y;
return dx * dx + dy * dy;
}
void limit_least(gfx::Rect& frame)
{
int min_distance = INT_MAX;
gfx::Rect candidate(frame); // it shouldn't be possible for there to be no workareas but set
// candidate to a valid Rect anyways
for (const auto& workarea : get_all_workareas()) {
// simulate clamping
gfx::Rect clone(frame);
limit_with_workarea(workarea, clone);
// find the least clamped clone
int distance = distance_squared(clone, frame);
if (distance < min_distance) {
min_distance = distance;
candidate.x = clone.x;
candidate.y = clone.y;
candidate.w = clone.w;
candidate.h = clone.h;
}
}
frame.x = candidate.x;
frame.y = candidate.y;
frame.w = candidate.w;
frame.h = candidate.h;
}
} // namespace ui

View File

@ -39,7 +39,8 @@ void fit_bounds(
std::function<gfx::Rect(Widget*)> getWidgetBounds)> fitLogic = nullptr);
// The "frame" is a native windows frame bounds.
void limit_with_workarea(Display* parentDisplay, gfx::Rect& frame);
void limit_with_workarea(const gfx::Rect& workareaBounds, gfx::Rect& frame);
void limit_least(gfx::Rect& frame);
} // namespace ui

View File

@ -39,6 +39,11 @@ namespace ui {
class Display;
// Class to render a widget in the screen.
//
// The gfx::Color parameter is a color in the sRGB color space
// (e.g. used to paint theme elements on widgets). If you want to
// paint a color from other color space, use the Paint version of each
// function.
class Graphics {
public:
Graphics(Display* display, const os::SurfaceRef& surface, int dx, int dy);
@ -47,7 +52,10 @@ public:
int width() const;
int height() const;
Display* display() const { return m_display; }
os::Surface* getInternalSurface() { return m_surface.get(); }
os::ColorSpace* colorSpace() { return m_surface->colorSpace().get(); }
int getInternalDeltaX() { return m_dx; }
int getInternalDeltaY() { return m_dy; }

View File

@ -1440,7 +1440,7 @@ void Manager::_openWindow(Window* window, bool center)
changeFrame = false;
}
limit_with_workarea(parentDisplay, frame);
limit_least(frame);
spec.position(os::WindowSpec::Position::Frame);
spec.frame(frame);

View File

@ -22,8 +22,6 @@
#include <string>
#include <vector>
#include <algorithm>
namespace ui {
#ifdef _WIN32
@ -377,25 +375,4 @@ bool Shortcut::isLooselyPressed() const
return false;
}
//////////////////////////////////////////////////////////////////////
// Shortcuts
bool Shortcuts::has(const Shortcut& shortcut) const
{
return (std::find(begin(), end(), shortcut) != end());
}
void Shortcuts::add(const Shortcut& shortcut)
{
if (!has(shortcut))
m_list.push_back(shortcut);
}
void Shortcuts::remove(const Shortcut& shortcut)
{
auto it = std::find(begin(), end(), shortcut);
if (it != end())
m_list.erase(it);
}
} // namespace ui

View File

@ -9,6 +9,7 @@
#define UI_SHORTCUT_H_INCLUDED
#pragma once
#include <algorithm>
#include <string>
#include <vector>
@ -51,11 +52,12 @@ private:
int m_unicodeChar;
};
class Shortcuts {
template<typename T>
class ShortcutsT {
public:
typedef std::vector<Shortcut> List;
typedef List::iterator iterator;
typedef List::const_iterator const_iterator;
using List = std::vector<T>;
using iterator = typename List::iterator;
using const_iterator = typename List::const_iterator;
iterator begin() { return m_list.begin(); }
iterator end() { return m_list.end(); }
@ -65,21 +67,39 @@ public:
bool empty() const { return m_list.empty(); }
std::size_t size() const { return m_list.size(); }
const ui::Shortcut& front() const { return m_list.front(); }
const T& front() const { return m_list.front(); }
const ui::Shortcut& operator[](int index) const { return m_list[index]; }
const T& operator[](int index) const { return m_list[index]; }
ui::Shortcut& operator[](int index) { return m_list[index]; }
T& operator[](int index) { return m_list[index]; }
void clear() { m_list.clear(); }
bool has(const Shortcut& shortcut) const;
void add(const Shortcut& shortcut);
void remove(const Shortcut& shortcut);
bool has(const T& shortcut) const { return (std::find(begin(), end(), shortcut) != end()); }
void push_back(const T& shortcut) { m_list.push_back(shortcut); }
void add(const T& shortcut)
{
if (!has(shortcut))
m_list.push_back(shortcut);
}
void remove(const T& shortcut)
{
auto it = std::find(begin(), end(), shortcut);
if (it != end())
m_list.erase(it);
}
iterator erase(const iterator& it) { return m_list.erase(it); }
private:
List m_list;
};
using Shortcuts = ShortcutsT<Shortcut>;
} // namespace ui
#endif

View File

@ -1391,7 +1391,8 @@ GraphicsPtr Widget::getGraphics(const gfx::Rect& clip)
// In case of double-buffering, we need to create the temporary
// buffer only if the default surface is the screen.
if (isDoubleBuffered() && dstSurface->isDirectToScreen()) {
os::SurfaceRef surface = os::System::instance()->makeSurface(clip.w, clip.h);
os::SurfaceRef surface =
os::System::instance()->makeSurface(clip.w, clip.h, dstSurface->colorSpace());
graphics.reset(new Graphics(display, surface, -clip.x, -clip.y),
DeleteGraphicsAndSurface(clip, surface, dstSurface));
}