mirror of https://github.com/aseprite/aseprite.git
Compare commits
29 Commits
79d3a1c3c9
...
50d515a8e9
Author | SHA1 | Date |
---|---|---|
|
50d515a8e9 | |
|
6d89a6bc15 | |
|
d4e97b5a96 | |
|
205b18dc0f | |
|
2ba051b59b | |
|
3fcb000eb1 | |
|
af9dc3c817 | |
|
250dfdc86a | |
|
c904c41b39 | |
|
9e941e9a8b | |
|
bbab4d5875 | |
|
5c4daff128 | |
|
11a7b061ff | |
|
283bedf77e | |
|
2eeb6f04a7 | |
|
706d0b8a7a | |
|
2f3a7f5dec | |
|
d5de74b715 | |
|
2d87a7b184 | |
|
2f22804fe8 | |
|
bf1b4c6f50 | |
|
250244c777 | |
|
b4555fc098 | |
|
8783135bf7 | |
|
1a6a39700e | |
|
ce742bcbc1 | |
|
3c350c3e67 | |
|
b2afbf91d5 | |
|
b1d42a24f8 |
|
@ -4,7 +4,7 @@ Aseprite is being developed and maintained currently by [Igara Studio](https://i
|
|||
The active team of developers is:
|
||||
|
||||
* [David Capello](https://github.com/dacap)
|
||||
* [Gaspar Capello](https://github.com/Gsparoken)
|
||||
* [Gaspar Capello](https://github.com/Gasparoken)
|
||||
* [Martín Capello](https://github.com/martincapello)
|
||||
* [Christian Kaiser](https://github.com/ckaiser)
|
||||
* [Dante Paola](https://github.com/Liebranca)
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
<option id="one_finger_as_mouse_movement" type="bool" default="true" />
|
||||
<option id="load_wintab_driver" type="bool" default="false" />
|
||||
<option id="flash_layer" type="bool" default="false" />
|
||||
<option id="use_selection_tool_loop" type="bool" default="false" />
|
||||
<option id="nonactive_layers_opacity" type="int" default="255" />
|
||||
<option id="nonactive_layers_opacity_preview" type="int" default="255" />
|
||||
</section>
|
||||
|
@ -356,6 +357,7 @@
|
|||
<section id="file_selector">
|
||||
<option id="current_folder" type="std::string" default=""<empty>"" />
|
||||
<option id="zoom" type="double" default="1.0" />
|
||||
<option id="show_hidden" type="bool" default="false" />
|
||||
</section>
|
||||
<section id="text_tool">
|
||||
<option id="font_face" type="std::string" />
|
||||
|
|
|
@ -765,6 +765,7 @@ pinned_folders = Pinned Folders
|
|||
recent_folders = Recent Folders
|
||||
all_formats = All formats
|
||||
all_files = All files
|
||||
show_hidden = Show hidden
|
||||
|
||||
[filters]
|
||||
selected_cels = Selected
|
||||
|
@ -1561,6 +1562,7 @@ set_cursor_fix = Set cursor position from stylus location
|
|||
set_cursor_fix_tooltip = Sets the mouse position to the pen location when\nyou have two pointers available (e.g. mouse and pen)\n\nUseful to zoom in/out from the pen position and to get the\ncorrect cursor location when screencasting/live streaming.
|
||||
wintab_more_info = (More Information)
|
||||
flash_selected_layer = Flash layer when it is selected
|
||||
use_selection_tool_loop = New selection tools implementation
|
||||
non_active_layer_opacity = Opacity for non-active layers:
|
||||
cel_content_format = Cel content format:
|
||||
cel_format_compressed = Compressed
|
||||
|
@ -1834,6 +1836,18 @@ pixel_scale = Pixel Scale
|
|||
with_vars = Use CSS3 Variables
|
||||
generate_html = Generate Sample HTML File
|
||||
|
||||
[shape]
|
||||
fill = Fill
|
||||
stroke = Stroke
|
||||
stroke_width = Stroke Width
|
||||
|
||||
[text_tool]
|
||||
font_family = Font Family
|
||||
font_size = Font Size
|
||||
bold = Bold
|
||||
italic = Italic
|
||||
more_options = More Options
|
||||
|
||||
[timeline_conf]
|
||||
position = Position:
|
||||
left = &Left
|
||||
|
@ -1896,6 +1910,7 @@ timeline_show = Show Timeline
|
|||
|
||||
[undo_history]
|
||||
title = Undo History
|
||||
initial_state = Initial State
|
||||
|
||||
[user_data]
|
||||
user_data = User Data:
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<combobox id="location" expansive="true" />
|
||||
<button text="" id="refresh_button" style="refresh_button"
|
||||
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
|
||||
<check id="show_hidden_check" text="@.show_hidden" />
|
||||
</box>
|
||||
<vbox id="file_view_placeholder" expansive="true" />
|
||||
<grid columns="2">
|
||||
|
|
|
@ -624,6 +624,7 @@
|
|||
text="@.hue_with_sat_value"
|
||||
pref="experimental.hue_with_sat_value_for_color_selector" />
|
||||
<check id="flash_layer" text="@.flash_selected_layer" />
|
||||
<check id="use_selection_tool_loop" text="@.use_selection_tool_loop" />
|
||||
<hbox>
|
||||
<label text="@.non_active_layer_opacity" />
|
||||
<slider id="nonactive_layers_opacity" min="0" max="255" width="128" />
|
||||
|
|
2
laf
2
laf
|
@ -1 +1 @@
|
|||
Subproject commit 7873e82be7828d1c22255a4b06d1d3d34586f12f
|
||||
Subproject commit a2bb9ec7fb98354279a2c49870a4a47a67a8e86e
|
|
@ -655,6 +655,7 @@ public:
|
|||
}
|
||||
#endif
|
||||
|
||||
useSelectionToolLoop()->setSelected(m_pref.experimental.useSelectionToolLoop());
|
||||
flashLayer()->setSelected(m_pref.experimental.flashLayer());
|
||||
nonactiveLayersOpacity()->setValue(m_pref.experimental.nonactiveLayersOpacity());
|
||||
|
||||
|
@ -886,6 +887,7 @@ public:
|
|||
m_pref.asepriteFormat.celFormat(gen::CelContentFormat(celFormat()->getSelectedItemIndex()));
|
||||
|
||||
// Experimental features
|
||||
m_pref.experimental.useSelectionToolLoop(useSelectionToolLoop()->isSelected());
|
||||
m_pref.experimental.flashLayer(flashLayer()->isSelected());
|
||||
m_pref.experimental.nonactiveLayersOpacity(nonactiveLayersOpacity()->getValue());
|
||||
m_pref.quantization.rgbmapAlgorithm(m_rgbmapAlgorithmSelector.algorithm());
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -10,6 +10,7 @@
|
|||
#endif
|
||||
|
||||
#include "app/app.h"
|
||||
#include "app/color_utils.h"
|
||||
#include "app/commands/command.h"
|
||||
#include "app/console.h"
|
||||
#include "app/context.h"
|
||||
|
@ -88,10 +89,10 @@ void PasteTextCommand::onExecute(Context* ctx)
|
|||
std::string text = window.userText()->text();
|
||||
app::Color color = window.fontColor()->getColor();
|
||||
|
||||
doc::ImageRef image = render_text(
|
||||
fontInfo,
|
||||
text,
|
||||
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
|
||||
ui::Paint paint = window.fontFace()->paint();
|
||||
paint.color(color_utils::color_for_ui(color));
|
||||
|
||||
doc::ImageRef image = render_text(fontInfo, text, paint);
|
||||
if (image) {
|
||||
Sprite* sprite = editor->sprite();
|
||||
if (image->pixelFormat() != sprite->pixelFormat()) {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include "app/doc_undo.h"
|
||||
#include "app/doc_undo_observer.h"
|
||||
#include "app/docs_observer.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/modules/gui.h"
|
||||
#include "app/modules/palettes.h"
|
||||
#include "app/site.h"
|
||||
|
@ -292,7 +293,7 @@ public:
|
|||
base::get_pretty_memory_size(static_cast<Cmd*>(state->cmd())->memSize())
|
||||
#endif
|
||||
:
|
||||
std::string("Initial State"));
|
||||
Strings::undo_history_initial_state());
|
||||
|
||||
if ((g->getClipBounds() & itemBounds).isEmpty())
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2023 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -139,6 +139,13 @@ DataRecovery::Sessions DataRecovery::sessions()
|
|||
return copy;
|
||||
}
|
||||
|
||||
bool DataRecovery::isRunningSession(const SessionPtr& session) const
|
||||
{
|
||||
ASSERT(session);
|
||||
ASSERT(m_inProgress);
|
||||
return session->path() == m_inProgress->path();
|
||||
}
|
||||
|
||||
void DataRecovery::searchForSessions()
|
||||
{
|
||||
Sessions sessions;
|
||||
|
@ -150,7 +157,7 @@ void DataRecovery::searchForSessions()
|
|||
RECO_TRACE("RECO: Session '%s' ", itempath.c_str());
|
||||
|
||||
SessionPtr session(new Session(&m_config, itempath));
|
||||
if (!session->isRunning()) {
|
||||
if (!isRunningSession(session)) {
|
||||
if ((session->isEmpty()) || (!session->isCrashedSession() && session->isOldSession())) {
|
||||
RECO_TRACE("to be deleted (%s)\n",
|
||||
session->isEmpty() ? "is empty" :
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -45,6 +45,8 @@ public:
|
|||
// Returns a copy of the list of sessions that can be recovered.
|
||||
Sessions sessions();
|
||||
|
||||
bool isRunningSession(const SessionPtr& session) const;
|
||||
|
||||
// Triggered in the UI-thread from the m_thread using an
|
||||
// ui::execute_from_ui_thread() when the list of sessions is ready
|
||||
// to be used.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -19,12 +19,10 @@
|
|||
#include "app/crash/write_document.h"
|
||||
#include "app/doc.h"
|
||||
#include "app/doc_access.h"
|
||||
#include "app/file/file.h"
|
||||
#include "app/ui_context.h"
|
||||
#include "base/convert_to.h"
|
||||
#include "base/fs.h"
|
||||
#include "base/fstream_path.h"
|
||||
#include "base/process.h"
|
||||
#include "base/split_string.h"
|
||||
#include "base/string.h"
|
||||
#include "base/thread.h"
|
||||
|
@ -128,12 +126,6 @@ const Session::Backups& Session::backups()
|
|||
return m_backups;
|
||||
}
|
||||
|
||||
bool Session::isRunning()
|
||||
{
|
||||
loadPid();
|
||||
return base::get_process_name(m_pid) == base::get_process_name(base::get_current_process_id());
|
||||
}
|
||||
|
||||
bool Session::isCrashedSession()
|
||||
{
|
||||
loadPid();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -47,9 +47,9 @@ public:
|
|||
|
||||
std::string name() const;
|
||||
std::string version();
|
||||
std::string& path() { return m_path; }
|
||||
const Backups& backups();
|
||||
|
||||
bool isRunning();
|
||||
bool isCrashedSession();
|
||||
bool isOldSession();
|
||||
bool isEmpty();
|
||||
|
|
|
@ -195,6 +195,7 @@ public:
|
|||
const MaskBoundaries& maskBoundaries() const { return m_maskBoundaries; }
|
||||
|
||||
MaskBoundaries& maskBoundaries() { return m_maskBoundaries; }
|
||||
void setMaskBoundaries(const MaskBoundaries& segs) { m_maskBoundaries = segs; }
|
||||
|
||||
bool hasMaskBoundaries() const { return !m_maskBoundaries.isEmpty(); }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -1401,7 +1401,7 @@ private:
|
|||
color_t color = *srcIt;
|
||||
int i;
|
||||
|
||||
if (rgba_geta(color) >= 128) {
|
||||
if (rgba_geta(color) > 0) {
|
||||
i = framePalette.findExactMatch(rgba_getr(color),
|
||||
rgba_getg(color),
|
||||
rgba_getb(color),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -82,6 +82,9 @@ public:
|
|||
unsigned int m_version;
|
||||
bool m_removed;
|
||||
mutable bool m_is_folder;
|
||||
#ifdef _WIN32
|
||||
bool m_isHidden = false;
|
||||
#endif
|
||||
std::atomic<double> m_thumbnailProgress;
|
||||
std::atomic<os::Surface*> m_thumbnail;
|
||||
#ifdef _WIN32
|
||||
|
@ -266,7 +269,7 @@ IFileItem* FileSystemModule::getRootFileItem()
|
|||
fileitem->m_pidl = pidl;
|
||||
fileitem->m_fullpidl = pidl;
|
||||
|
||||
SFGAOF attrib = SFGAO_FOLDER;
|
||||
SFGAOF attrib = SFGAO_FOLDER | SFGAO_HIDDEN;
|
||||
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
|
||||
|
||||
update_by_pidl(fileitem, attrib);
|
||||
|
@ -357,7 +360,7 @@ bool FileItem::isHidden() const
|
|||
ASSERT(m_displayname != NOTINITIALIZED);
|
||||
|
||||
#ifdef _WIN32
|
||||
return false;
|
||||
return m_isHidden;
|
||||
#else
|
||||
return m_displayname[0] == '.';
|
||||
#endif
|
||||
|
@ -462,7 +465,7 @@ const FileItemList& FileItem::children()
|
|||
// Get the interface to enumerate subitems
|
||||
hr = pFolder->EnumObjects(
|
||||
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
|
||||
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
|
||||
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN,
|
||||
&pEnum);
|
||||
|
||||
if (hr == S_OK && pEnum) {
|
||||
|
@ -473,10 +476,9 @@ const FileItemList& FileItem::children()
|
|||
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
|
||||
// Request the SFGAO_FOLDER attribute to know what of the
|
||||
// item is file or a folder
|
||||
for (c = 0; c < fetched; ++c) {
|
||||
attribs[c] = SFGAO_FOLDER;
|
||||
pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)itempidl, attribs + c);
|
||||
}
|
||||
for (c = 0; c < fetched; ++c)
|
||||
attribs[c] = SFGAO_FOLDER | SFGAO_HIDDEN;
|
||||
pFolder->GetAttributesOf(fetched, (LPCITEMIDLIST*)itempidl, attribs);
|
||||
|
||||
// Generate the FileItems
|
||||
for (c = 0; c < fetched; ++c) {
|
||||
|
@ -755,6 +757,9 @@ static void update_by_pidl(FileItem* fileitem, SFGAOF attrib)
|
|||
// Is it a folder?
|
||||
|
||||
fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);
|
||||
#if _WIN32
|
||||
fileitem->m_isHidden = (attrib & SFGAO_HIDDEN ? true : false);
|
||||
#endif
|
||||
|
||||
// Get the name to display
|
||||
|
||||
|
|
|
@ -291,9 +291,15 @@ public:
|
|||
if (m_createSlice)
|
||||
m_maxBounds |= rc;
|
||||
else {
|
||||
rc &= loop->getDstImage()->bounds();
|
||||
for (int v = rc.y; v < rc.y2(); ++v)
|
||||
BaseInk::inkHline(rc.x, v, rc.x2() - 1, loop);
|
||||
if (loop->isSelectionToolLoop()) {
|
||||
rc &= loop->sprite()->bounds();
|
||||
loop->addSelectionToolPoint(rc);
|
||||
}
|
||||
else {
|
||||
rc &= loop->getDstImage()->bounds();
|
||||
for (int v = rc.y; v < rc.y2(); ++v)
|
||||
BaseInk::inkHline(rc.x, v, rc.x2() - 1, loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,7 +310,8 @@ public:
|
|||
m_maxBounds = gfx::Rect(0, 0, 0, 0);
|
||||
}
|
||||
else {
|
||||
m_maxBounds &= loop->getDstImage()->bounds();
|
||||
m_maxBounds &= (loop->isSelectionToolLoop() ? loop->sprite()->bounds() :
|
||||
loop->getDstImage()->bounds());
|
||||
loop->onSliceRect(m_maxBounds);
|
||||
}
|
||||
}
|
||||
|
@ -456,9 +463,15 @@ public:
|
|||
m_maxBounds |= rc;
|
||||
}
|
||||
else {
|
||||
rc &= loop->getDstImage()->bounds();
|
||||
for (int v = rc.y; v < rc.y2(); ++v)
|
||||
BaseInk::inkHline(rc.x, v, rc.x2() - 1, loop);
|
||||
if (loop->isSelectionToolLoop()) {
|
||||
rc &= loop->sprite()->bounds();
|
||||
loop->addSelectionToolPoint(rc);
|
||||
}
|
||||
else {
|
||||
rc &= loop->getDstImage()->bounds();
|
||||
for (int v = rc.y; v < rc.y2(); ++v)
|
||||
BaseInk::inkHline(rc.x, v, rc.x2() - 1, loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,8 +29,10 @@ void PointShape::doInkHline(int x1, int y, int x2, ToolLoop* loop)
|
|||
{
|
||||
Ink* ink = loop->getInk();
|
||||
TiledMode tiledMode = loop->getTiledMode();
|
||||
const int dstw = loop->getDstImage()->width();
|
||||
const int dsth = loop->getDstImage()->height();
|
||||
const int dstw = (!loop->isSelectionToolLoop() ? loop->getDstImage()->width() :
|
||||
loop->sprite()->bounds().w);
|
||||
const int dsth = (!loop->isSelectionToolLoop() ? loop->getDstImage()->height() :
|
||||
loop->sprite()->bounds().h);
|
||||
int x, w, size; // width or height
|
||||
|
||||
// In case the ink needs original cel coordinates, we have to
|
||||
|
|
|
@ -264,6 +264,11 @@ public:
|
|||
virtual void onSliceRect(const gfx::Rect& bounds) = 0;
|
||||
|
||||
virtual const app::TiledModeHelper& getTiledModeHelper() = 0;
|
||||
|
||||
// Used by selection tools
|
||||
virtual bool isSelectionToolLoop() const = 0;
|
||||
virtual void addSelectionToolPoint(const gfx::Rect& rc) = 0;
|
||||
virtual void clearSelectionToolMask(const bool finalStep) = 0;
|
||||
};
|
||||
|
||||
} // namespace tools
|
||||
|
|
|
@ -271,6 +271,8 @@ void ToolLoopManager::doLoopStep(bool lastStep)
|
|||
}
|
||||
|
||||
m_toolLoop->validateDstImage(m_dirtyArea);
|
||||
if (!lastStep && m_toolLoop->isSelectionToolLoop())
|
||||
m_toolLoop->clearSelectionToolMask(false);
|
||||
|
||||
// Join or fill user points
|
||||
if (fillStrokes)
|
||||
|
@ -341,6 +343,13 @@ void ToolLoopManager::calculateDirtyArea(const Strokes& strokes)
|
|||
m_dirtyArea.createUnion(m_dirtyArea, Region(r1.createUnion(r2)));
|
||||
}
|
||||
|
||||
// This ensures the Selection Tool Mask doesn't leave a 'trail' behind
|
||||
if (m_toolLoop->isSelectionToolLoop()) {
|
||||
auto bounds = m_dirtyArea.bounds();
|
||||
bounds.enlarge(1);
|
||||
m_dirtyArea |= gfx::Region(bounds);
|
||||
}
|
||||
|
||||
// Merge new dirty area with the previous one (for tools like line
|
||||
// or rectangle it's needed to redraw the previous position and
|
||||
// the new one)
|
||||
|
|
|
@ -1857,7 +1857,7 @@ private:
|
|||
|
||||
class ContextBar::FontSelector : public FontEntry {
|
||||
public:
|
||||
FontSelector(ContextBar* contextBar)
|
||||
FontSelector(ContextBar* contextBar) : FontEntry(true) // With stroke and fill options
|
||||
{
|
||||
// Load the font from the preferences
|
||||
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
|
||||
|
@ -2559,6 +2559,11 @@ FontInfo ContextBar::fontInfo() const
|
|||
return m_fontSelector->info();
|
||||
}
|
||||
|
||||
FontEntry* ContextBar::fontEntry()
|
||||
{
|
||||
return m_fontSelector;
|
||||
}
|
||||
|
||||
render::DitheringMatrix ContextBar::ditheringMatrix()
|
||||
{
|
||||
return m_ditheringSelector->ditheringMatrix();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -90,6 +90,7 @@ public:
|
|||
|
||||
// For text tool
|
||||
FontInfo fontInfo() const;
|
||||
FontEntry* fontEntry();
|
||||
|
||||
// For gradients
|
||||
render::DitheringMatrix ditheringMatrix();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -457,6 +457,8 @@ void BrushPreview::show(const gfx::Point& screenPos)
|
|||
|
||||
// Here we re-use the cached surface
|
||||
if (!cached && m_uiLayer->surface()) {
|
||||
m_uiLayer->surface()->clear();
|
||||
|
||||
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
|
||||
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);
|
||||
|
||||
|
|
|
@ -219,6 +219,8 @@ bool DrawingState::onMouseUp(Editor* editor, MouseMessage* msg)
|
|||
if (m_toolLoopManager->releaseButton(m_lastPointer))
|
||||
return true;
|
||||
}
|
||||
if (m_toolLoop->isSelectionToolLoop())
|
||||
m_toolLoop->clearSelectionToolMask(true);
|
||||
|
||||
destroyLoop(editor);
|
||||
|
||||
|
@ -403,6 +405,9 @@ void DrawingState::destroyLoopIfCanceled(Editor* editor)
|
|||
{
|
||||
// Cancel drawing loop
|
||||
if (m_toolLoopManager->isCanceled()) {
|
||||
if (m_toolLoop->isSelectionToolLoop())
|
||||
m_toolLoop->clearSelectionToolMask(true);
|
||||
|
||||
destroyLoop(editor);
|
||||
|
||||
// Change to standby state
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
#include "app/util/tile_flags_utils.h"
|
||||
#include "base/chrono.h"
|
||||
#include "base/convert_to.h"
|
||||
#include "base/scoped_value.h"
|
||||
#include "doc/doc.h"
|
||||
#include "doc/mask_boundaries.h"
|
||||
#include "doc/slice.h"
|
||||
|
@ -134,6 +135,7 @@ private:
|
|||
|
||||
// static
|
||||
Editor* Editor::m_activeEditor = nullptr;
|
||||
std::unique_ptr<Mask> Editor::m_selectionToolMask = nullptr;
|
||||
|
||||
// static
|
||||
std::unique_ptr<EditorRender> Editor::m_renderEngine = nullptr;
|
||||
|
@ -266,6 +268,23 @@ void Editor::setStateInternal(const EditorStatePtr& newState)
|
|||
{
|
||||
m_brushPreview.hide();
|
||||
|
||||
// Some onLeaveState impls (like the ones from MovingPixelsState,
|
||||
// WritingTextState, MovingSelectionState) might generate a
|
||||
// Tx/Transaction::commit(), which will add a new undo state,
|
||||
// triggering a sprite change scripting event
|
||||
// (SpriteEvents::onAddUndoState). This event could be handled by an
|
||||
// extension and that extension might want to save the current
|
||||
// sprite (e.g. calling Sprite_saveCopyAs, the kind of extension
|
||||
// that takes snapshots after each sprite change). That will be a
|
||||
// new Context::executeCommand() for the save command, generating a
|
||||
// BeforeCommandExecution signal, getting back to onLeaveState
|
||||
// again. In that case, we just ignore the reentry as the first
|
||||
// onLeaveState should handle everything (to avoid an stack
|
||||
// overflow/infinite recursion).
|
||||
if (m_leavingState)
|
||||
return;
|
||||
base::ScopedValue leaving(m_leavingState, true);
|
||||
|
||||
// Fire before change state event, set the state, and fire after
|
||||
// change state event.
|
||||
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());
|
||||
|
@ -962,6 +981,13 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
|
|||
m_proj.applyY(m_sprite->height()));
|
||||
gfx::Rect enclosingRect = spriteRect;
|
||||
|
||||
// Redraw the background when the selection tool mask draws over it
|
||||
static bool redrawBackground = false;
|
||||
if (redrawBackground) {
|
||||
drawBackground(g);
|
||||
redrawBackground = false;
|
||||
}
|
||||
|
||||
// Draw the main sprite at the center.
|
||||
drawOneSpriteUnclippedRect(g, rc, 0, 0);
|
||||
|
||||
|
@ -1013,8 +1039,24 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
|
|||
}
|
||||
|
||||
// Draw the mask
|
||||
if (m_document->hasMaskBoundaries())
|
||||
drawMask(g);
|
||||
const bool haveSegs = m_document->hasMaskBoundaries();
|
||||
if (haveSegs)
|
||||
drawMask(g, MaskIndex::Document);
|
||||
|
||||
if (hasSelectionToolMask()) {
|
||||
const auto segs = m_document->maskBoundaries();
|
||||
m_document->generateMaskBoundaries(m_selectionToolMask.get());
|
||||
drawMask(g, MaskIndex::SelectionTool);
|
||||
|
||||
if (haveSegs)
|
||||
m_document->setMaskBoundaries(segs);
|
||||
else
|
||||
m_document->destroyMaskBoundaries();
|
||||
|
||||
const gfx::Point prevPoint(m_selectionToolMask->bounds().point2());
|
||||
if (prevPoint.x >= m_sprite->width() || prevPoint.y >= m_sprite->height())
|
||||
redrawBackground = true;
|
||||
}
|
||||
|
||||
// Post-render decorator.
|
||||
if ((m_flags & kShowDecorators) && m_decorator) {
|
||||
|
@ -1049,9 +1091,10 @@ void Editor::drawSpriteClipped(const gfx::Region& updateRegion)
|
|||
* regenerate boundaries, use the sprite_generate_mask_boundaries()
|
||||
* routine.
|
||||
*/
|
||||
void Editor::drawMask(Graphics* g)
|
||||
void Editor::drawMask(Graphics* g, const MaskIndex index)
|
||||
{
|
||||
if ((m_flags & kShowMask) == 0 || !m_docPref.show.selectionEdges())
|
||||
if (index == MaskIndex::Document &&
|
||||
((m_flags & kShowMask) == 0 || !m_docPref.show.selectionEdges()))
|
||||
return;
|
||||
|
||||
ASSERT(m_document->hasMaskBoundaries());
|
||||
|
@ -1066,10 +1109,15 @@ void Editor::drawMask(Graphics* g)
|
|||
|
||||
ui::Paint paint;
|
||||
paint.style(ui::Paint::Stroke);
|
||||
set_checkered_paint_mode(paint,
|
||||
m_antsOffset,
|
||||
gfx::rgba(0, 0, 0, 255),
|
||||
gfx::rgba(255, 255, 255, 255));
|
||||
if (index == MaskIndex::Document) {
|
||||
set_checkered_paint_mode(paint,
|
||||
m_antsOffset,
|
||||
gfx::rgba(0, 0, 0, 255),
|
||||
gfx::rgba(255, 255, 255, 255));
|
||||
}
|
||||
else {
|
||||
set_checkered_paint_mode(paint, 0, gfx::rgba(0, 0, 0, 255), gfx::rgba(255, 255, 255, 255));
|
||||
}
|
||||
|
||||
// We translate the path instead of applying a matrix to the
|
||||
// ui::Graphics so the "checkered" pattern is not scaled too.
|
||||
|
@ -1081,10 +1129,11 @@ void Editor::drawMask(Graphics* g)
|
|||
|
||||
void Editor::drawMaskSafe()
|
||||
{
|
||||
if ((m_flags & kShowMask) == 0)
|
||||
if (((m_flags & kShowMask) == 0 && !hasSelectionToolMask()) || !(isVisible() && m_document))
|
||||
return;
|
||||
|
||||
if (isVisible() && m_document && m_document->hasMaskBoundaries()) {
|
||||
const bool haveSegs = m_document->hasMaskBoundaries();
|
||||
if (haveSegs || hasSelectionToolMask()) {
|
||||
Region region;
|
||||
getDrawableRegion(region, kCutTopWindows);
|
||||
region.offset(-bounds().origin());
|
||||
|
@ -1092,10 +1141,27 @@ void Editor::drawMaskSafe()
|
|||
HideBrushPreview hide(m_brushPreview);
|
||||
GraphicsPtr g = getGraphics(clientBounds());
|
||||
|
||||
for (const gfx::Rect& rc : region) {
|
||||
IntersectClip clip(g.get(), rc);
|
||||
if (clip)
|
||||
drawMask(g.get());
|
||||
if (haveSegs) {
|
||||
for (const gfx::Rect& rc : region) {
|
||||
IntersectClip clip(g.get(), rc);
|
||||
if (clip)
|
||||
drawMask(g.get(), MaskIndex::Document);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSelectionToolMask()) {
|
||||
const auto segs = m_document->maskBoundaries();
|
||||
m_document->generateMaskBoundaries(m_selectionToolMask.get());
|
||||
for (const gfx::Rect& rc : region) {
|
||||
IntersectClip clip(g.get(), rc);
|
||||
if (clip)
|
||||
drawMask(g.get(), MaskIndex::SelectionTool);
|
||||
}
|
||||
|
||||
if (haveSegs)
|
||||
m_document->setMaskBoundaries(segs);
|
||||
else
|
||||
m_document->destroyMaskBoundaries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1994,6 +2060,21 @@ void Editor::showUnhandledException(const std::exception& ex, const ui::Message*
|
|||
(state ? typeid(*state).name() : "None"));
|
||||
}
|
||||
|
||||
void Editor::makeSelectionToolMask()
|
||||
{
|
||||
m_selectionToolMask.reset(new Mask());
|
||||
}
|
||||
|
||||
void Editor::deleteSelectionToolMask()
|
||||
{
|
||||
m_selectionToolMask.reset();
|
||||
}
|
||||
|
||||
bool Editor::hasSelectionToolMask()
|
||||
{
|
||||
return m_selectionToolMask && !m_selectionToolMask->isEmpty();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Message handler for the editor
|
||||
|
||||
|
@ -2377,7 +2458,7 @@ void Editor::onPaint(ui::PaintEvent& ev)
|
|||
|
||||
// Draw the mask boundaries
|
||||
if (m_document->hasMaskBoundaries()) {
|
||||
drawMask(g);
|
||||
drawMask(g, MaskIndex::Document);
|
||||
m_antsTimer.start();
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -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
|
||||
|
@ -317,6 +317,11 @@ public:
|
|||
// an Editor or EditorState event.
|
||||
void showUnhandledException(const std::exception& ex, const ui::Message* msg);
|
||||
|
||||
Mask* getSelectionToolMask() { return m_selectionToolMask.get(); }
|
||||
void makeSelectionToolMask();
|
||||
void deleteSelectionToolMask();
|
||||
bool hasSelectionToolMask();
|
||||
|
||||
static void registerCommands();
|
||||
|
||||
protected:
|
||||
|
@ -361,7 +366,8 @@ private:
|
|||
void drawBackground(ui::Graphics* g);
|
||||
void drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& rc);
|
||||
void drawMaskSafe();
|
||||
void drawMask(ui::Graphics* g);
|
||||
enum MaskIndex { Document, SelectionTool, Count };
|
||||
void drawMask(ui::Graphics* g, MaskIndex index);
|
||||
void drawGrid(ui::Graphics* g,
|
||||
const gfx::Rect& spriteBounds,
|
||||
const gfx::Rect& gridBounds,
|
||||
|
@ -458,6 +464,13 @@ private:
|
|||
|
||||
DocView* m_docView;
|
||||
|
||||
// Special flag to avoid re-entering a new state when we are leaving
|
||||
// the current one. This avoids an infinite onLeaveState() recursion
|
||||
// in some special cases when an extension (third-party code)
|
||||
// creates a new sprite change in the same sprite change scripting
|
||||
// event.
|
||||
bool m_leavingState = false;
|
||||
|
||||
// Last known mouse position received by this editor when the
|
||||
// mouse button was pressed. Used for auto-scrolling. To get the
|
||||
// current mouse position on the editor you can use
|
||||
|
@ -504,6 +517,9 @@ private:
|
|||
// same document can show the same preview image/stroke being drawn
|
||||
// (search for Render::setPreviewImage()).
|
||||
static std::unique_ptr<EditorRender> m_renderEngine;
|
||||
|
||||
// Used for selection tool feedback
|
||||
static std::unique_ptr<Mask> m_selectionToolMask;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
|
|
@ -428,8 +428,8 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
|
|||
if (editor->slicesTransforms())
|
||||
drawExtraCel();
|
||||
|
||||
// Redraw the editor.
|
||||
editor->invalidate();
|
||||
// Notify changes
|
||||
m_site.document()->notifyGeneralUpdate();
|
||||
|
||||
// Use StandbyState implementation
|
||||
return StandbyState::onMouseMove(editor, msg);
|
||||
|
|
|
@ -426,6 +426,11 @@ public:
|
|||
|
||||
const app::TiledModeHelper& getTiledModeHelper() override { return m_tiledModeHelper; }
|
||||
|
||||
// Used by selection tools
|
||||
bool isSelectionToolLoop() const override { return false; }
|
||||
void addSelectionToolPoint(const gfx::Rect& rc) override {};
|
||||
void clearSelectionToolMask(const bool finalStep) override {};
|
||||
|
||||
protected:
|
||||
void updateAllVisibleRegion()
|
||||
{
|
||||
|
@ -444,31 +449,26 @@ protected:
|
|||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// For drawing
|
||||
// Common properties between drawing/selection ToolLoop impl
|
||||
|
||||
class ToolLoopImpl final : public ToolLoopBase,
|
||||
public EditorObserver {
|
||||
class PaintToolLoopBase : public ToolLoopBase,
|
||||
public EditorObserver {
|
||||
protected:
|
||||
Context* m_context;
|
||||
bool m_filled;
|
||||
bool m_previewFilled;
|
||||
int m_sprayWidth;
|
||||
int m_spraySpeed;
|
||||
bool m_useMask;
|
||||
Mask* m_mask;
|
||||
gfx::Point m_maskOrigin;
|
||||
bool m_internalCancel = false;
|
||||
Tx m_tx;
|
||||
std::unique_ptr<ExpandCelCanvas> m_expandCelCanvas;
|
||||
Image* m_floodfillSrcImage;
|
||||
bool m_saveLastPoint;
|
||||
|
||||
public:
|
||||
ToolLoopImpl(Editor* editor,
|
||||
Site& site,
|
||||
const doc::Grid& grid,
|
||||
Context* context,
|
||||
ToolLoopParams& params,
|
||||
const bool saveLastPoint)
|
||||
PaintToolLoopBase(Editor* editor,
|
||||
Site& site,
|
||||
const doc::Grid& grid,
|
||||
Context* context,
|
||||
ToolLoopParams& params,
|
||||
const bool saveLastPoint)
|
||||
: ToolLoopBase(editor, site, grid, params)
|
||||
, m_context(context)
|
||||
, m_tx(Tx::DontLockDoc,
|
||||
|
@ -481,9 +481,19 @@ public:
|
|||
ModifyDocument))
|
||||
, m_floodfillSrcImage(nullptr)
|
||||
, m_saveLastPoint(saveLastPoint)
|
||||
{
|
||||
}
|
||||
|
||||
~PaintToolLoopBase()
|
||||
{
|
||||
if (m_editor)
|
||||
m_editor->remove_observer(this);
|
||||
}
|
||||
|
||||
void constructor_prologue()
|
||||
{
|
||||
if (m_pointShape->isFloodFill()) {
|
||||
if (m_tilesMode) {
|
||||
if (m_tilesMode && !isSelectionToolLoop()) {
|
||||
// This will be set later to getSrcImage()
|
||||
m_floodfillSrcImage = nullptr;
|
||||
}
|
||||
|
@ -502,7 +512,104 @@ public:
|
|||
else if (Cel* cel = m_layer->cel(m_frame)) {
|
||||
m_floodfillSrcImage = render::rasterize_with_sprite_bounds(cel);
|
||||
}
|
||||
else if (isSelectionToolLoop() && !m_layer->cel(m_frame)) {
|
||||
m_floodfillSrcImage =
|
||||
Image::create(m_sprite->pixelFormat(), m_sprite->width(), m_sprite->height());
|
||||
|
||||
m_floodfillSrcImage->clear(m_sprite->transparentColor());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void constructor_epilogue()
|
||||
{
|
||||
m_mask = m_document->mask();
|
||||
m_maskOrigin = (!m_mask->isEmpty() ? gfx::Point(m_mask->bounds().x - m_celOrigin.x,
|
||||
m_mask->bounds().y - m_celOrigin.y) :
|
||||
gfx::Point(0, 0));
|
||||
|
||||
if (m_editor)
|
||||
m_editor->add_observer(this);
|
||||
}
|
||||
|
||||
// IToolLoop interface
|
||||
bool needsCelCoordinates() override
|
||||
{
|
||||
if (m_tilesMode) {
|
||||
// When we are painting with tiles, we don't need to adjust the
|
||||
// coordinates by the cel position in PointShape (points will be
|
||||
// in tiles position relative to the tilemap origin already).
|
||||
return false;
|
||||
}
|
||||
else
|
||||
return ToolLoopBase::needsCelCoordinates();
|
||||
}
|
||||
|
||||
const Image* getFloodFillSrcImage() override { return m_floodfillSrcImage; }
|
||||
Mask* getMask() override { return m_mask; }
|
||||
void setMask(Mask* newMask) override { m_tx(new cmd::SetMask(m_document, newMask)); }
|
||||
gfx::Point getMaskOrigin() override { return m_maskOrigin; }
|
||||
|
||||
void onSliceRect(const gfx::Rect& bounds) override
|
||||
{
|
||||
// TODO add support for slice tool from batch scripts without UI?
|
||||
if (m_editor && getMouseButton() == ToolLoop::Left) {
|
||||
// Try to select slices, but if it returns false, it means that
|
||||
// there are no slices in the box to be selected, so we show a
|
||||
// popup menu to create a new one.
|
||||
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
|
||||
Slice* slice = new Slice;
|
||||
slice->setName(getUniqueSliceName());
|
||||
|
||||
SliceKey key(bounds);
|
||||
slice->insert(getFrame(), key);
|
||||
|
||||
auto color = Preferences::instance().slices.defaultColor();
|
||||
slice->userData().setColor(
|
||||
doc::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
|
||||
|
||||
m_tx(new cmd::AddSlice(m_sprite, slice));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel the operation (do not create a new transaction for this
|
||||
// no-op, e.g. just change the set of selected slices).
|
||||
m_internalCancel = true;
|
||||
}
|
||||
|
||||
private:
|
||||
// EditorObserver impl
|
||||
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||
|
||||
std::string getUniqueSliceName() const
|
||||
{
|
||||
std::string prefix = "Slice";
|
||||
int max = 0;
|
||||
|
||||
for (Slice* slice : m_sprite->slices())
|
||||
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
|
||||
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
|
||||
|
||||
return fmt::format("{} {}", prefix, max + 1);
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// For drawing
|
||||
|
||||
class ToolLoopImpl final : public PaintToolLoopBase {
|
||||
public:
|
||||
ToolLoopImpl(Editor* editor,
|
||||
Site& site,
|
||||
const doc::Grid& grid,
|
||||
Context* context,
|
||||
ToolLoopParams& params,
|
||||
const bool saveLastPoint)
|
||||
: PaintToolLoopBase(editor, site, grid, context, params, saveLastPoint)
|
||||
{
|
||||
constructor_prologue();
|
||||
|
||||
// 'isSelectionPreview = true' if the intention is to show a preview
|
||||
// of Selection tools or Slice tool.
|
||||
|
@ -547,7 +654,7 @@ public:
|
|||
// Start with an empty mask if the user is selecting with "default selection mode"
|
||||
if (isSelectionPreview &&
|
||||
(!m_document->isMaskVisible() ||
|
||||
(int(getModifiers()) & int(tools::ToolLoopModifiers::kReplaceSelection)))) {
|
||||
(int(getModifiers()) & int(tools::ToolLoopModifiers::kReplaceSelection)) != 0)) {
|
||||
Mask emptyMask;
|
||||
m_tx(new cmd::SetMask(m_document, &emptyMask));
|
||||
}
|
||||
|
@ -557,20 +664,11 @@ public:
|
|||
m_grid = m_expandCelCanvas->getGrid();
|
||||
m_celOrigin = m_expandCelCanvas->getCelOrigin();
|
||||
|
||||
m_mask = m_document->mask();
|
||||
m_maskOrigin = (!m_mask->isEmpty() ? gfx::Point(m_mask->bounds().x - m_celOrigin.x,
|
||||
m_mask->bounds().y - m_celOrigin.y) :
|
||||
gfx::Point(0, 0));
|
||||
|
||||
if (m_editor)
|
||||
m_editor->add_observer(this);
|
||||
constructor_epilogue();
|
||||
}
|
||||
|
||||
~ToolLoopImpl()
|
||||
{
|
||||
if (m_editor)
|
||||
m_editor->remove_observer(this);
|
||||
|
||||
// getSrcImage() is a virtual member function but ToolLoopImpl is
|
||||
// marked as final to avoid not calling a derived version from
|
||||
// this destructor.
|
||||
|
@ -579,18 +677,6 @@ public:
|
|||
}
|
||||
|
||||
// IToolLoop interface
|
||||
bool needsCelCoordinates() override
|
||||
{
|
||||
if (m_tilesMode) {
|
||||
// When we are painting with tiles, we don't need to adjust the
|
||||
// coordinates by the cel position in PointShape (points will be
|
||||
// in tiles position relative to the tilemap origin already).
|
||||
return false;
|
||||
}
|
||||
else
|
||||
return ToolLoopBase::needsCelCoordinates();
|
||||
}
|
||||
|
||||
void commit() override
|
||||
{
|
||||
bool redraw = false;
|
||||
|
@ -650,7 +736,6 @@ public:
|
|||
|
||||
const Cel* getCel() override { return m_expandCelCanvas->getCel(); }
|
||||
const Image* getSrcImage() override { return m_expandCelCanvas->getSourceCanvas(); }
|
||||
const Image* getFloodFillSrcImage() override { return m_floodfillSrcImage; }
|
||||
Image* getDstImage() override { return m_expandCelCanvas->getDestCanvas(); }
|
||||
Tileset* getDstTileset() override { return m_expandCelCanvas->getDestTileset(); }
|
||||
void validateSrcImage(const gfx::Region& rgn) override
|
||||
|
@ -676,58 +761,110 @@ public:
|
|||
}
|
||||
|
||||
bool useMask() override { return m_useMask; }
|
||||
Mask* getMask() override { return m_mask; }
|
||||
void setMask(Mask* newMask) override { m_tx(new cmd::SetMask(m_document, newMask)); }
|
||||
gfx::Point getMaskOrigin() override { return m_maskOrigin; }
|
||||
bool getFilled() override { return m_filled; }
|
||||
bool getPreviewFilled() override { return m_previewFilled; }
|
||||
int getSprayWidth() override { return m_sprayWidth; }
|
||||
int getSpraySpeed() override { return m_spraySpeed; }
|
||||
|
||||
void onSliceRect(const gfx::Rect& bounds) override
|
||||
private:
|
||||
bool m_filled;
|
||||
bool m_previewFilled;
|
||||
int m_sprayWidth;
|
||||
int m_spraySpeed;
|
||||
bool m_useMask;
|
||||
std::unique_ptr<ExpandCelCanvas> m_expandCelCanvas;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// For selection tools
|
||||
|
||||
class SelectionToolLoopImpl final : public PaintToolLoopBase {
|
||||
public:
|
||||
SelectionToolLoopImpl(Editor* editor,
|
||||
Site& site,
|
||||
const doc::Grid& grid,
|
||||
Context* context,
|
||||
ToolLoopParams& params,
|
||||
const bool saveLastPoint)
|
||||
: PaintToolLoopBase(editor, site, grid, context, params, saveLastPoint)
|
||||
{
|
||||
// TODO add support for slice tool from batch scripts without UI?
|
||||
if (m_editor && getMouseButton() == ToolLoop::Left) {
|
||||
// Try to select slices, but if it returns false, it means that
|
||||
// there are no slices in the box to be selected, so we show a
|
||||
// popup menu to create a new one.
|
||||
if (!m_editor->selectSliceBox(bounds) && (bounds.w > 1 || bounds.h > 1)) {
|
||||
Slice* slice = new Slice;
|
||||
slice->setName(getUniqueSliceName());
|
||||
constructor_prologue();
|
||||
|
||||
SliceKey key(bounds);
|
||||
slice->insert(getFrame(), key);
|
||||
|
||||
auto color = Preferences::instance().slices.defaultColor();
|
||||
slice->userData().setColor(
|
||||
doc::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
|
||||
|
||||
m_tx(new cmd::AddSlice(m_sprite, slice));
|
||||
return;
|
||||
}
|
||||
// Start with an empty mask if the user is selecting with "default selection mode"
|
||||
if (!m_document->isMaskVisible() ||
|
||||
(int(getModifiers()) & int(tools::ToolLoopModifiers::kReplaceSelection)) != 0) {
|
||||
Mask emptyMask;
|
||||
m_tx(new cmd::SetMask(m_document, &emptyMask));
|
||||
}
|
||||
|
||||
// Cancel the operation (do not create a new transaction for this
|
||||
// no-op, e.g. just change the set of selected slices).
|
||||
m_internalCancel = true;
|
||||
m_editor->makeSelectionToolMask();
|
||||
constructor_epilogue();
|
||||
}
|
||||
|
||||
private:
|
||||
// EditorObserver impl
|
||||
void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||
void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
|
||||
|
||||
std::string getUniqueSliceName() const
|
||||
~SelectionToolLoopImpl()
|
||||
{
|
||||
std::string prefix = "Slice";
|
||||
int max = 0;
|
||||
|
||||
for (Slice* slice : m_sprite->slices())
|
||||
if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
|
||||
max = std::max(max, (int)std::strtol(slice->name().c_str() + prefix.size(), nullptr, 10));
|
||||
|
||||
return fmt::format("{} {}", prefix, max + 1);
|
||||
m_editor->deleteSelectionToolMask();
|
||||
delete m_floodfillSrcImage;
|
||||
}
|
||||
|
||||
// For drawing the selection to second mask
|
||||
bool isSelectionToolLoop() const override { return true; }
|
||||
void addSelectionToolPoint(const gfx::Rect& rc) override
|
||||
{
|
||||
if (rc.w >= 1 && rc.h >= 1)
|
||||
m_editor->getSelectionToolMask()->add(rc);
|
||||
}
|
||||
|
||||
void clearSelectionToolMask(const bool finalStep) override
|
||||
{
|
||||
if (finalStep || getTracePolicy() == tools::TracePolicy::Last)
|
||||
m_editor->getSelectionToolMask()->clear();
|
||||
}
|
||||
|
||||
// IToolLoop interface
|
||||
void commit() override
|
||||
{
|
||||
bool redraw = false;
|
||||
|
||||
if (!m_internalCancel) {
|
||||
// Freehand changes the last point
|
||||
if (m_saveLastPoint) {
|
||||
m_tx(new cmd::SetLastPoint(m_document, getController()->getLastPoint().toPoint()));
|
||||
}
|
||||
|
||||
// Selection ink
|
||||
redraw = true;
|
||||
if (m_ink->isSelection() && Preferences::instance().selection.autoShowSelectionEdges()) {
|
||||
// Show selection edges
|
||||
m_docPref.show.selectionEdges(true);
|
||||
}
|
||||
|
||||
m_tx.commit();
|
||||
}
|
||||
else {
|
||||
rollback();
|
||||
}
|
||||
|
||||
if (redraw)
|
||||
update_screen_for_document(m_document);
|
||||
}
|
||||
void rollback() override {}
|
||||
|
||||
const Image* getSrcImage() override { return m_floodfillSrcImage; }
|
||||
Image* getDstImage() override { return nullptr; }
|
||||
Tileset* getDstTileset() override { return nullptr; }
|
||||
void validateSrcImage(const gfx::Region& rgn) override {}
|
||||
void validateDstImage(const gfx::Region& rgn) override {}
|
||||
void validateDstTileset(const gfx::Region& rgn) override {}
|
||||
void invalidateDstImage() override {}
|
||||
void invalidateDstImage(const gfx::Region& rgn) override {}
|
||||
void copyValidDstToSrcImage(const gfx::Region& rgn) override {}
|
||||
|
||||
bool useMask() override { return false; }
|
||||
bool getFilled() override { return true; }
|
||||
bool getPreviewFilled() override { return getTracePolicy() == tools::TracePolicy::Last; }
|
||||
int getSprayWidth() override { return 0; }
|
||||
int getSpraySpeed() override { return 0; }
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
@ -850,8 +987,16 @@ tools::ToolLoop* create_tool_loop(Editor* editor,
|
|||
fill_toolloop_params_from_tool_preferences(params);
|
||||
|
||||
ASSERT(context->activeDocument() == editor->document());
|
||||
auto toolLoop = new ToolLoopImpl(editor, site, grid, context, params, saveLastPoint);
|
||||
if (Preferences::instance().experimental.useSelectionToolLoop() &&
|
||||
(params.ink->isSelection() || params.ink->isSlice())) {
|
||||
auto* toolLoop =
|
||||
new SelectionToolLoopImpl(editor, site, grid, context, params, saveLastPoint);
|
||||
if (selectTiles)
|
||||
toolLoop->forceSnapToTiles();
|
||||
|
||||
return toolLoop;
|
||||
}
|
||||
auto* toolLoop = new ToolLoopImpl(editor, site, grid, context, params, saveLastPoint);
|
||||
if (selectTiles)
|
||||
toolLoop->forceSnapToTiles();
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include "app/commands/command.h"
|
||||
#include "app/extra_cel.h"
|
||||
#include "app/fonts/font_info.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/site.h"
|
||||
#include "app/tx.h"
|
||||
|
@ -43,12 +44,42 @@
|
|||
#include "os/skia/skia_surface.h"
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace app {
|
||||
|
||||
using namespace ui;
|
||||
|
||||
// Get ui::Paint to render text from context bar options / preferences
|
||||
static ui::Paint get_paint_for_text()
|
||||
{
|
||||
ui::Paint paint;
|
||||
if (auto* app = App::instance()) {
|
||||
if (auto* ctxBar = app->contextBar())
|
||||
paint = ctxBar->fontEntry()->paint();
|
||||
}
|
||||
paint.color(color_utils::color_for_ui(Preferences::instance().colorBar.fgColor()));
|
||||
return paint;
|
||||
}
|
||||
|
||||
static gfx::RectF calc_blob_bounds(const text::TextBlobRef& blob)
|
||||
{
|
||||
gfx::RectF bounds = get_text_blob_required_bounds(blob);
|
||||
ui::Paint paint = get_paint_for_text();
|
||||
if (paint.style() == ui::Paint::Style::Stroke ||
|
||||
paint.style() == ui::Paint::Style::StrokeAndFill) {
|
||||
bounds.enlarge(std::ceil(paint.strokeWidth()));
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
class WritingTextState::TextEditor : public Entry {
|
||||
public:
|
||||
enum TextPreview {
|
||||
Intermediate, // With selection preview / user interface
|
||||
Final, // Final to be rendered in the cel
|
||||
};
|
||||
|
||||
TextEditor(Editor* editor, const Site& site, const gfx::Rect& bounds)
|
||||
: Entry(4096, "")
|
||||
, m_editor(editor)
|
||||
|
@ -61,7 +92,7 @@ public:
|
|||
setPersistSelection(true);
|
||||
|
||||
createExtraCel(site, bounds);
|
||||
renderExtraCelBase();
|
||||
renderExtraCel(TextPreview::Intermediate);
|
||||
|
||||
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
|
||||
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
|
||||
|
@ -76,36 +107,37 @@ public:
|
|||
|
||||
// Returns the extra cel with the text rendered (but without the
|
||||
// selected text highlighted).
|
||||
ExtraCelRef extraCel()
|
||||
ExtraCelRef extraCel(const TextPreview textPreview)
|
||||
{
|
||||
renderExtraCelBase();
|
||||
renderExtraCelText(false);
|
||||
renderExtraCel(textPreview);
|
||||
return m_extraCel;
|
||||
}
|
||||
|
||||
void setExtraCelBounds(const gfx::Rect& bounds)
|
||||
void setExtraCelBounds(const gfx::RectF& bounds)
|
||||
{
|
||||
doc::Image* extraImg = m_extraCel->image();
|
||||
if (!extraImg || bounds.w != extraImg->width() || bounds.h != extraImg->height()) {
|
||||
if (!extraImg || std::ceil(bounds.w) != extraImg->width() ||
|
||||
std::ceil(bounds.h) != extraImg->height()) {
|
||||
createExtraCel(m_editor->getSite(), bounds);
|
||||
}
|
||||
else {
|
||||
m_baseBounds = bounds;
|
||||
m_extraCel->cel()->setBounds(bounds);
|
||||
}
|
||||
renderExtraCelBase();
|
||||
renderExtraCelText(true);
|
||||
renderExtraCel(TextPreview::Intermediate);
|
||||
}
|
||||
|
||||
obs::signal<void(const gfx::Size&)> NewRequiredBounds;
|
||||
obs::signal<void(const gfx::RectF&)> NewRequiredBounds;
|
||||
|
||||
private:
|
||||
void createExtraCel(const Site& site, const gfx::Rect& bounds)
|
||||
{
|
||||
m_baseBounds = bounds;
|
||||
m_extraCel->create(ExtraCel::Purpose::TextPreview,
|
||||
site.tilemapMode(),
|
||||
site.sprite(),
|
||||
bounds,
|
||||
bounds.size(),
|
||||
gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h)),
|
||||
site.frame(),
|
||||
255);
|
||||
|
||||
|
@ -176,7 +208,7 @@ private:
|
|||
|
||||
// Notify that we could make the text editor bigger to show this
|
||||
// text blob.
|
||||
NewRequiredBounds(get_text_blob_required_size(blob));
|
||||
NewRequiredBounds(calc_blob_bounds(blob));
|
||||
}
|
||||
|
||||
void onPaint(PaintEvent& ev) override
|
||||
|
@ -205,8 +237,7 @@ private:
|
|||
}
|
||||
|
||||
// Render extra cel with text + selected text
|
||||
renderExtraCelBase();
|
||||
renderExtraCelText(true);
|
||||
renderExtraCel(TextPreview::Intermediate);
|
||||
m_doc->setExtraCel(m_extraCel);
|
||||
|
||||
// Paint caret
|
||||
|
@ -227,76 +258,80 @@ private:
|
|||
}
|
||||
}
|
||||
|
||||
void renderExtraCelBase()
|
||||
void renderExtraCel(const TextPreview textPreview)
|
||||
{
|
||||
doc::Image* extraImg = m_extraCel->image();
|
||||
ASSERT(extraImg);
|
||||
if (!extraImg)
|
||||
return;
|
||||
|
||||
const doc::Cel* extraCel = m_extraCel->cel();
|
||||
extraImg->clear(extraImg->maskColor());
|
||||
|
||||
text::TextBlobRef blob = textBlob();
|
||||
doc::ImageRef blobImage;
|
||||
gfx::RectF bounds;
|
||||
if (blob) {
|
||||
const ui::Paint paint = get_paint_for_text();
|
||||
bounds = calc_blob_bounds(blob);
|
||||
blobImage = render_text_blob(blob, bounds, get_paint_for_text());
|
||||
if (!blobImage)
|
||||
return;
|
||||
|
||||
// Invert selected range in the image
|
||||
if (textPreview == TextPreview::Intermediate) {
|
||||
Range range;
|
||||
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
|
||||
if (!range.isEmpty()) {
|
||||
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
|
||||
|
||||
if (!selectedBounds.isEmpty()) {
|
||||
selectedBounds.offset(-bounds.origin());
|
||||
|
||||
#ifdef LAF_SKIA
|
||||
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(blobImage.get());
|
||||
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
|
||||
|
||||
os::Paint paint2 = paint;
|
||||
paint2.blendMode(os::BlendMode::Xor);
|
||||
paint2.style(os::Paint::Style::Fill);
|
||||
surface->drawRect(selectedBounds, paint2);
|
||||
#endif // LAF_SKIA
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc::Cel* extraCel = m_extraCel->cel();
|
||||
ASSERT(extraCel);
|
||||
if (!extraCel)
|
||||
return;
|
||||
|
||||
extraImg->clear(extraImg->maskColor());
|
||||
extraCel->setPosition(m_baseBounds.x + bounds.x, m_baseBounds.y + bounds.y);
|
||||
|
||||
render::Render().renderLayer(extraImg,
|
||||
m_editor->layer(),
|
||||
m_editor->frame(),
|
||||
gfx::Clip(0, 0, extraCel->bounds()),
|
||||
doc::BlendMode::SRC);
|
||||
}
|
||||
|
||||
void renderExtraCelText(const bool withSelection)
|
||||
{
|
||||
const auto textColor = color_utils::color_for_image(Preferences::instance().colorBar.fgColor(),
|
||||
IMAGE_RGB);
|
||||
|
||||
text::TextBlobRef blob = textBlob();
|
||||
if (!blob)
|
||||
return;
|
||||
|
||||
doc::ImageRef image = render_text_blob(blob, textColor);
|
||||
if (!image)
|
||||
return;
|
||||
|
||||
// Invert selected range in the image
|
||||
if (withSelection) {
|
||||
Range range;
|
||||
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
|
||||
if (!range.isEmpty()) {
|
||||
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
|
||||
|
||||
if (!selectedBounds.isEmpty()) {
|
||||
#ifdef LAF_SKIA
|
||||
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
|
||||
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
|
||||
|
||||
os::Paint paint;
|
||||
paint.blendMode(os::BlendMode::Xor);
|
||||
paint.color(textColor);
|
||||
surface->drawRect(selectedBounds, paint);
|
||||
#endif // LAF_SKIA
|
||||
}
|
||||
}
|
||||
if (blobImage) {
|
||||
doc::blend_image(extraImg,
|
||||
blobImage.get(),
|
||||
gfx::Clip(blobImage->bounds().size()),
|
||||
m_doc->sprite()->palette(m_editor->frame()),
|
||||
255,
|
||||
doc::BlendMode::NORMAL);
|
||||
}
|
||||
|
||||
doc::Image* extraImg = m_extraCel->image();
|
||||
ASSERT(extraImg);
|
||||
if (!extraImg)
|
||||
return;
|
||||
|
||||
doc::blend_image(extraImg,
|
||||
image.get(),
|
||||
gfx::Clip(image->bounds().size()),
|
||||
m_doc->sprite()->palette(m_editor->frame()),
|
||||
255,
|
||||
doc::BlendMode::NORMAL);
|
||||
}
|
||||
|
||||
Editor* m_editor;
|
||||
Doc* m_doc;
|
||||
ExtraCelRef m_extraCel;
|
||||
|
||||
// Initial bounds for the entry field. This can be modified later to
|
||||
// render the text in case some initial letter/glyph needs some
|
||||
// extra room at the left side.
|
||||
gfx::Rect m_baseBounds;
|
||||
};
|
||||
|
||||
WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
|
||||
|
@ -312,10 +347,10 @@ WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
|
|||
m_fontChangeConn =
|
||||
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
|
||||
|
||||
m_entry->NewRequiredBounds.connect([this](const gfx::Size& blobSize) {
|
||||
if (m_bounds.w < blobSize.w || m_bounds.h < blobSize.h) {
|
||||
m_bounds.w = std::max(m_bounds.w, blobSize.w);
|
||||
m_bounds.h = std::max(m_bounds.h, blobSize.h);
|
||||
m_entry->NewRequiredBounds.connect([this](const gfx::RectF& blobBounds) {
|
||||
if (m_bounds.w < blobBounds.w || m_bounds.h < blobBounds.h) {
|
||||
m_bounds.w = std::max(m_bounds.w, blobBounds.w);
|
||||
m_bounds.h = std::max(m_bounds.h, blobBounds.h);
|
||||
m_entry->setExtraCelBounds(m_bounds);
|
||||
m_entry->setBounds(calcEntryBounds());
|
||||
}
|
||||
|
@ -388,11 +423,11 @@ void WritingTextState::onCommitMouseMove(Editor* editor, const gfx::PointF& spri
|
|||
if (!m_movingBounds)
|
||||
return;
|
||||
|
||||
gfx::Point delta(spritePos - m_cursorStart);
|
||||
gfx::PointF delta(spritePos - m_cursorStart);
|
||||
if (delta.x == 0 && delta.y == 0)
|
||||
return;
|
||||
|
||||
m_bounds.setOrigin(gfx::Point(delta + m_boundsOrigin));
|
||||
m_bounds.setOrigin(delta + m_boundsOrigin);
|
||||
m_entry->setExtraCelBounds(m_bounds);
|
||||
m_entry->setBounds(calcEntryBounds());
|
||||
}
|
||||
|
@ -475,8 +510,8 @@ EditorState::LeaveAction WritingTextState::onLeaveState(Editor* editor, EditorSt
|
|||
// Paints the text in the active layer/sprite creating an
|
||||
// undoable transaction.
|
||||
Site site = m_editor->getSite();
|
||||
ExtraCelRef extraCel = m_entry->extraCel();
|
||||
Tx tx(site.document(), "Text Tool");
|
||||
ExtraCelRef extraCel = m_entry->extraCel(TextEditor::Final);
|
||||
Tx tx(site.document(), Strings::tools_text());
|
||||
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
|
||||
|
||||
expand.validateDestCanvas(gfx::Region(extraCel->cel()->bounds()));
|
||||
|
@ -527,7 +562,7 @@ void WritingTextState::onFontChange(const FontInfo& fontInfo, FontEntry::From fr
|
|||
|
||||
// This is useful to show changes to the anti-alias option
|
||||
// immediately.
|
||||
auto dummy = m_entry->extraCel();
|
||||
auto dummy = m_entry->extraCel(TextEditor::Intermediate);
|
||||
|
||||
if (fromField == FontEntry::From::Popup) {
|
||||
if (m_entry)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// 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.
|
||||
|
@ -59,7 +59,7 @@ private:
|
|||
|
||||
DelayedMouseMove m_delayedMouseMove;
|
||||
Editor* m_editor;
|
||||
gfx::Rect m_bounds;
|
||||
gfx::RectF m_bounds;
|
||||
std::unique_ptr<TextEditor> m_entry;
|
||||
|
||||
// True if the text was discarded.
|
||||
|
@ -71,7 +71,7 @@ private:
|
|||
bool m_mouseMoveReceived = false;
|
||||
bool m_movingBounds = false;
|
||||
gfx::PointF m_cursorStart;
|
||||
gfx::Point m_boundsOrigin;
|
||||
gfx::PointF m_boundsOrigin;
|
||||
|
||||
obs::scoped_connection m_beforeCmdConn;
|
||||
obs::scoped_connection m_fontChangeConn;
|
||||
|
|
|
@ -45,6 +45,7 @@ FileList::FileList()
|
|||
, m_multiselect(false)
|
||||
, m_zoom(1.0)
|
||||
, m_itemsPerRow(0)
|
||||
, m_showHidden(Preferences::instance().fileSelector.showHidden())
|
||||
{
|
||||
setFocusStop(true);
|
||||
setDoubleBuffered(true);
|
||||
|
@ -173,6 +174,14 @@ void FileList::animateToZoom(const double zoom)
|
|||
startAnimation(ANI_ZOOM, 10);
|
||||
}
|
||||
|
||||
void FileList::setShowHidden(const bool show)
|
||||
{
|
||||
m_showHidden = show;
|
||||
m_req_valid = false;
|
||||
m_selected = nullptr;
|
||||
regenerateList();
|
||||
}
|
||||
|
||||
bool FileList::onProcessMessage(Message* msg)
|
||||
{
|
||||
switch (msg->type()) {
|
||||
|
@ -825,7 +834,7 @@ void FileList::regenerateList()
|
|||
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
|
||||
IFileItem* fileitem = *it;
|
||||
|
||||
if (fileitem->isHidden())
|
||||
if (fileitem->isHidden() && !m_showHidden)
|
||||
it = m_list.erase(it);
|
||||
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
|
||||
it = m_list.erase(it);
|
||||
|
|
|
@ -54,6 +54,7 @@ public:
|
|||
double zoom() const { return m_zoom; }
|
||||
void setZoom(const double zoom);
|
||||
void animateToZoom(const double zoom);
|
||||
void setShowHidden(const bool show);
|
||||
|
||||
obs::signal<void()> FileSelected;
|
||||
obs::signal<void()> FileAccepted;
|
||||
|
@ -137,6 +138,7 @@ private:
|
|||
double m_toZoom;
|
||||
|
||||
int m_itemsPerRow;
|
||||
bool m_showHidden;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
|
|
@ -316,6 +316,7 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
|
|||
for (auto child : viewType()->children())
|
||||
child->setFocusStop(false);
|
||||
|
||||
showHiddenCheck()->setSelected(Preferences::instance().fileSelector.showHidden());
|
||||
m_fileList = new FileList();
|
||||
m_fileList->setId("fileview");
|
||||
m_fileName->setAssociatedFileList(m_fileList);
|
||||
|
@ -334,6 +335,10 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
|
|||
viewType()->ItemChange.connect([this] { onChangeViewType(); });
|
||||
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
|
||||
fileType()->Change.connect([this] { onFileTypeChange(); });
|
||||
showHiddenCheck()->Click.connect([this] {
|
||||
Preferences::instance().fileSelector.showHidden(showHiddenCheck()->isSelected());
|
||||
m_fileList->setShowHidden(showHiddenCheck()->isSelected());
|
||||
});
|
||||
m_fileList->FileSelected.connect([this] { onFileListFileSelected(); });
|
||||
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
|
||||
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2022 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.
|
||||
|
@ -10,8 +10,10 @@
|
|||
|
||||
#include "app/ui/filename_field.h"
|
||||
|
||||
#include "app/app.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/recent_files.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "base/fs.h"
|
||||
#include "ui/box.h"
|
||||
|
@ -24,6 +26,11 @@ namespace app {
|
|||
|
||||
using namespace ui;
|
||||
|
||||
FilenameField::FilenameButton::FilenameButton(const std::string& text) : ButtonSet(1)
|
||||
{
|
||||
addItem(text);
|
||||
}
|
||||
|
||||
FilenameField::FilenameField(const Type type, const std::string& pathAndFilename)
|
||||
: m_entry(type == EntryAndButton ? new ui::Entry(1024, "") : nullptr)
|
||||
, m_button(type == EntryAndButton ? Strings::select_file_browse() : Strings::select_file_text())
|
||||
|
@ -46,7 +53,10 @@ FilenameField::FilenameField(const Type type, const std::string& pathAndFilename
|
|||
if (m_entry)
|
||||
m_entry->Change.connect([this] { setFilename(updatedFilename()); });
|
||||
|
||||
m_button.Click.connect([this] { onBrowse(); });
|
||||
m_button.ItemChange.connect([this](ButtonSet::Item* item) {
|
||||
m_button.setSelectedItem(nullptr);
|
||||
onBrowse();
|
||||
});
|
||||
initTheme();
|
||||
|
||||
m_editFullPathChangeConn = Preferences::instance().general.editFullPath.AfterChange.connect(
|
||||
|
@ -94,7 +104,6 @@ void FilenameField::onSetEditFullPath()
|
|||
void FilenameField::onBrowse()
|
||||
{
|
||||
const gfx::Rect bounds = m_button.bounds();
|
||||
m_button.setSelected(false);
|
||||
|
||||
ui::Menu menu;
|
||||
ui::MenuItem choose(Strings::select_file_choose());
|
||||
|
@ -107,6 +116,11 @@ void FilenameField::onBrowse()
|
|||
menu.addChild(&relative);
|
||||
menu.addChild(&absolute);
|
||||
|
||||
if (auto* recent = App::instance()->recentFiles()) {
|
||||
addFoldersToMenu(&menu, recent->pinnedFolders(), Strings::file_selector_pinned_folders());
|
||||
addFoldersToMenu(&menu, recent->recentFolders(), Strings::file_selector_recent_folders());
|
||||
}
|
||||
|
||||
choose.Click.connect([this] {
|
||||
std::string fn = SelectOutputFile();
|
||||
if (!fn.empty()) {
|
||||
|
@ -120,6 +134,21 @@ void FilenameField::onBrowse()
|
|||
menu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
|
||||
}
|
||||
|
||||
void FilenameField::addFoldersToMenu(ui::Menu* menu,
|
||||
const base::paths& folders,
|
||||
const std::string& separatorTitle)
|
||||
{
|
||||
if (folders.empty())
|
||||
return;
|
||||
|
||||
menu->addChild(new ui::Separator(separatorTitle, ui::HORIZONTAL));
|
||||
for (const std::string& folder : folders) {
|
||||
MenuItem* folderItem = new MenuItem(folder);
|
||||
folderItem->Click.connect([this, folder] { setFilename(base::join_path(folder, m_file)); });
|
||||
menu->addChild(folderItem);
|
||||
}
|
||||
}
|
||||
|
||||
void FilenameField::setFilename(const std::string& pathAndFilename)
|
||||
{
|
||||
const std::string spritePath = base::get_file_path(m_docFilename);
|
||||
|
@ -164,11 +193,6 @@ void FilenameField::onInitTheme(ui::InitThemeEvent& ev)
|
|||
{
|
||||
HBox::onInitTheme(ev);
|
||||
setChildSpacing(0);
|
||||
|
||||
auto theme = skin::SkinTheme::get(this);
|
||||
ui::Style* style = theme->styles.miniButton();
|
||||
if (style)
|
||||
m_button.setStyle(style);
|
||||
}
|
||||
|
||||
void FilenameField::onUpdateText()
|
||||
|
@ -181,9 +205,9 @@ void FilenameField::updateWidgets()
|
|||
if (m_entry)
|
||||
m_entry->setText(displayedFilename());
|
||||
else if (m_file.empty())
|
||||
m_button.setText(Strings::select_file_text());
|
||||
m_button.getItem(0)->setText(Strings::select_file_text());
|
||||
else
|
||||
m_button.setText(displayedFilename());
|
||||
m_button.getItem(0)->setText(displayedFilename());
|
||||
|
||||
Change();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019 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.
|
||||
|
@ -8,14 +8,19 @@
|
|||
#define APP_UI_FILENAME_FIELD_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/ui/button_set.h"
|
||||
#include "base/paths.h"
|
||||
#include "obs/connection.h"
|
||||
#include "obs/signal.h"
|
||||
#include "ui/box.h"
|
||||
#include "ui/button.h"
|
||||
#include "ui/entry.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ui {
|
||||
class Menu;
|
||||
}
|
||||
|
||||
namespace app {
|
||||
|
||||
class FilenameField : public ui::HBox {
|
||||
|
@ -44,17 +49,25 @@ protected:
|
|||
void onSetEditFullPath();
|
||||
|
||||
private:
|
||||
class FilenameButton : public ButtonSet {
|
||||
public:
|
||||
FilenameButton(const std::string& text);
|
||||
};
|
||||
|
||||
void setEditFullPath(const bool on);
|
||||
void updateWidgets();
|
||||
void onBrowse();
|
||||
std::string updatedFilename() const;
|
||||
void addFoldersToMenu(ui::Menu* menu,
|
||||
const base::paths& folders,
|
||||
const std::string& separatorTitle);
|
||||
|
||||
std::string m_path;
|
||||
std::string m_pathBase;
|
||||
std::string m_file;
|
||||
std::string m_docFilename;
|
||||
ui::Entry* m_entry;
|
||||
ui::Button m_button;
|
||||
FilenameButton m_button;
|
||||
bool m_editFullPath;
|
||||
bool m_askOverwrite;
|
||||
|
||||
|
|
|
@ -12,10 +12,12 @@
|
|||
|
||||
#include "app/app.h"
|
||||
#include "app/console.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/recent_files.h"
|
||||
#include "app/ui/font_popup.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "base/contains.h"
|
||||
#include "base/convert_to.h"
|
||||
#include "base/scoped_value.h"
|
||||
#include "fmt/format.h"
|
||||
#include "ui/display.h"
|
||||
|
@ -262,22 +264,91 @@ void FontEntry::FontSize::onEntryChange()
|
|||
Change();
|
||||
}
|
||||
|
||||
FontEntry::FontStyle::FontStyle() : ButtonSet(3, true)
|
||||
FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, true)
|
||||
{
|
||||
addItem("B");
|
||||
addItem("I");
|
||||
addItem("...");
|
||||
setMultiMode(MultiMode::Set);
|
||||
|
||||
tooltips->addTooltipFor(getItem(0), Strings::text_tool_bold(), BOTTOM);
|
||||
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
|
||||
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
|
||||
}
|
||||
|
||||
FontEntry::FontEntry()
|
||||
FontEntry::FontStroke::FontStroke(ui::TooltipManager* tooltips) : m_fill(2)
|
||||
{
|
||||
auto* theme = skin::SkinTheme::get(this);
|
||||
|
||||
m_fill.addItem(theme->parts.toolFilledRectangle(), theme->styles.contextBarButton());
|
||||
m_fill.addItem(theme->parts.toolRectangle(), theme->styles.contextBarButton());
|
||||
m_fill.setSelectedItem(0);
|
||||
m_fill.ItemChange.connect([this] { Change(); });
|
||||
|
||||
m_stroke.setText("0");
|
||||
m_stroke.setSuffix("pt");
|
||||
m_stroke.ValueChange.connect([this] { Change(); });
|
||||
|
||||
addChild(&m_fill);
|
||||
addChild(&m_stroke);
|
||||
|
||||
tooltips->addTooltipFor(m_fill.getItem(0), Strings::shape_fill(), BOTTOM);
|
||||
tooltips->addTooltipFor(m_fill.getItem(1), Strings::shape_stroke(), BOTTOM);
|
||||
tooltips->addTooltipFor(&m_stroke, Strings::shape_stroke_width(), BOTTOM);
|
||||
}
|
||||
|
||||
bool FontEntry::FontStroke::fill() const
|
||||
{
|
||||
return const_cast<FontStroke*>(this)->m_fill.getItem(0)->isSelected();
|
||||
}
|
||||
|
||||
float FontEntry::FontStroke::stroke() const
|
||||
{
|
||||
return m_stroke.textDouble();
|
||||
}
|
||||
|
||||
FontEntry::FontStroke::WidthEntry::WidthEntry() : ui::IntEntry(0, 100, this)
|
||||
{
|
||||
}
|
||||
|
||||
void FontEntry::FontStroke::WidthEntry::onValueChange()
|
||||
{
|
||||
ui::IntEntry::onValueChange();
|
||||
ValueChange();
|
||||
}
|
||||
|
||||
bool FontEntry::FontStroke::WidthEntry::onAcceptUnicodeChar(int unicodeChar)
|
||||
{
|
||||
return (IntEntry::onAcceptUnicodeChar(unicodeChar) || unicodeChar == '.');
|
||||
}
|
||||
|
||||
std::string FontEntry::FontStroke::WidthEntry::onGetTextFromValue(int value)
|
||||
{
|
||||
return fmt::format("{:.1f}", value / 10.0);
|
||||
}
|
||||
|
||||
int FontEntry::FontStroke::WidthEntry::onGetValueFromText(const std::string& text)
|
||||
{
|
||||
return int(10.0 * base::convert_to<double>(text));
|
||||
}
|
||||
|
||||
FontEntry::FontEntry(const bool withStrokeAndFill)
|
||||
: m_style(&m_tooltips)
|
||||
, m_stroke(withStrokeAndFill ? std::make_unique<FontStroke>(&m_tooltips) : nullptr)
|
||||
{
|
||||
m_face.setExpansive(true);
|
||||
m_size.setExpansive(false);
|
||||
m_style.setExpansive(false);
|
||||
|
||||
addChild(&m_tooltips);
|
||||
addChild(&m_face);
|
||||
addChild(&m_size);
|
||||
addChild(&m_style);
|
||||
if (m_stroke)
|
||||
addChild(m_stroke.get());
|
||||
|
||||
m_tooltips.addTooltipFor(&m_face, Strings::text_tool_font_family(), BOTTOM);
|
||||
m_tooltips.addTooltipFor(m_size.getEntryWidget(), Strings::text_tool_font_size(), BOTTOM);
|
||||
|
||||
m_face.setMinSize(gfx::Size(128 * guiscale(), 0));
|
||||
|
||||
|
@ -299,6 +370,8 @@ FontEntry::FontEntry()
|
|||
});
|
||||
|
||||
m_style.ItemChange.connect(&FontEntry::onStyleItemClick, this);
|
||||
if (m_stroke)
|
||||
m_stroke->Change.connect(&FontEntry::onStrokeChange, this);
|
||||
}
|
||||
|
||||
// Defined here as FontPopup type is not fully defined in the header
|
||||
|
@ -327,6 +400,29 @@ void FontEntry::setInfo(const FontInfo& info, const From fromField)
|
|||
FontChange(m_info, fromField);
|
||||
}
|
||||
|
||||
ui::Paint FontEntry::paint()
|
||||
{
|
||||
ui::Paint paint;
|
||||
ui::Paint::Style style = ui::Paint::Fill;
|
||||
|
||||
if (m_stroke) {
|
||||
const float stroke = m_stroke->stroke();
|
||||
if (m_stroke->fill()) {
|
||||
if (stroke > 0.0f) {
|
||||
style = ui::Paint::StrokeAndFill;
|
||||
paint.strokeWidth(stroke);
|
||||
}
|
||||
}
|
||||
else {
|
||||
style = ui::Paint::Stroke;
|
||||
paint.strokeWidth(stroke);
|
||||
}
|
||||
}
|
||||
|
||||
paint.style(style);
|
||||
return paint;
|
||||
}
|
||||
|
||||
void FontEntry::onStyleItemClick(ButtonSet::Item* item)
|
||||
{
|
||||
text::FontStyle style = m_info.style();
|
||||
|
@ -404,4 +500,9 @@ void FontEntry::onStyleItemClick(ButtonSet::Item* item)
|
|||
}
|
||||
}
|
||||
|
||||
void FontEntry::onStrokeChange()
|
||||
{
|
||||
FontChange(m_info, From::Paint);
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
|
@ -14,7 +14,11 @@
|
|||
#include "ui/box.h"
|
||||
#include "ui/button.h"
|
||||
#include "ui/combobox.h"
|
||||
#include "ui/int_entry.h"
|
||||
#include "ui/paint.h"
|
||||
#include "ui/tooltips.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
@ -30,18 +34,22 @@ public:
|
|||
Flags,
|
||||
Hinting,
|
||||
Popup,
|
||||
Paint,
|
||||
};
|
||||
|
||||
FontEntry();
|
||||
FontEntry(bool withStrokeAndFill);
|
||||
~FontEntry();
|
||||
|
||||
FontInfo info() { return m_info; }
|
||||
void setInfo(const FontInfo& info, From from);
|
||||
|
||||
ui::Paint paint();
|
||||
|
||||
obs::signal<void(const FontInfo&, From)> FontChange;
|
||||
|
||||
private:
|
||||
void onStyleItemClick(ButtonSet::Item* item);
|
||||
void onStrokeChange();
|
||||
|
||||
class FontFace : public SearchEntry {
|
||||
public:
|
||||
|
@ -73,13 +81,40 @@ private:
|
|||
|
||||
class FontStyle : public ButtonSet {
|
||||
public:
|
||||
FontStyle();
|
||||
FontStyle(ui::TooltipManager* tooltips);
|
||||
};
|
||||
|
||||
class FontStroke : public HBox {
|
||||
public:
|
||||
FontStroke(ui::TooltipManager* tooltips);
|
||||
bool fill() const;
|
||||
float stroke() const;
|
||||
obs::signal<void()> Change;
|
||||
|
||||
private:
|
||||
class WidthEntry : public ui::IntEntry,
|
||||
public ui::SliderDelegate {
|
||||
public:
|
||||
WidthEntry();
|
||||
obs::signal<void()> ValueChange;
|
||||
|
||||
private:
|
||||
void onValueChange() override;
|
||||
bool onAcceptUnicodeChar(int unicodeChar) override;
|
||||
// SliderDelegate impl
|
||||
std::string onGetTextFromValue(int value) override;
|
||||
int onGetValueFromText(const std::string& text) override;
|
||||
};
|
||||
ButtonSet m_fill;
|
||||
WidthEntry m_stroke;
|
||||
};
|
||||
|
||||
ui::TooltipManager m_tooltips;
|
||||
FontInfo m_info;
|
||||
FontFace m_face;
|
||||
FontSize m_size;
|
||||
FontStyle m_style;
|
||||
std::unique_ptr<FontStroke> m_stroke;
|
||||
bool m_lockFace = false;
|
||||
};
|
||||
|
||||
|
|
|
@ -194,7 +194,11 @@ private:
|
|||
if (!blob)
|
||||
return;
|
||||
|
||||
doc::ImageRef image = render_text_blob(blob, gfx::rgba(0, 0, 0));
|
||||
ui::Paint paint;
|
||||
paint.color(gfx::rgba(0, 0, 0));
|
||||
paint.style(ui::Paint::Fill);
|
||||
const gfx::RectF textBounds = get_text_blob_required_bounds(blob);
|
||||
doc::ImageRef image = render_text_blob(blob, textBounds, paint);
|
||||
if (!image)
|
||||
return;
|
||||
|
||||
|
|
|
@ -1350,6 +1350,7 @@ public:
|
|||
, m_h(h)
|
||||
{
|
||||
m_widget->getEntryThemeInfo(&m_index, &m_caret, &m_state, &m_range);
|
||||
m_suffixIndex = m_widget->text().size();
|
||||
}
|
||||
|
||||
int index() const { return m_index; }
|
||||
|
@ -1369,6 +1370,11 @@ public:
|
|||
bg = ColorNone;
|
||||
fg = colors.text();
|
||||
|
||||
// Suffix text
|
||||
if (m_index >= m_suffixIndex) {
|
||||
fg = colors.entrySuffix();
|
||||
}
|
||||
|
||||
// Selected
|
||||
if ((m_index >= m_range.from) && (m_index < m_range.to)) {
|
||||
if (m_widget->hasFocus())
|
||||
|
@ -1433,6 +1439,7 @@ private:
|
|||
int m_lastX; // Last position used to fill the background
|
||||
int m_y, m_h;
|
||||
int m_charStartX;
|
||||
int m_suffixIndex;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
@ -1445,8 +1452,10 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
|
|||
DrawEntryTextDelegate delegate(widget, g, bounds.origin(), widget->textHeight());
|
||||
int scroll = delegate.index();
|
||||
|
||||
if (!widget->text().empty()) {
|
||||
const std::string& textString = widget->text();
|
||||
// Full text to paint: widget text + suffix
|
||||
const std::string textString = widget->text() + widget->getSuffix();
|
||||
|
||||
if (!textString.empty()) {
|
||||
base::utf8_decode dec(textString);
|
||||
auto pos = dec.pos();
|
||||
for (int i = 0; i < scroll && dec.next(); ++i)
|
||||
|
@ -1454,38 +1463,28 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
|
|||
|
||||
IntersectClip clip(g, bounds);
|
||||
if (clip) {
|
||||
g->drawTextWithDelegate(
|
||||
std::string(pos, textString.end()), // TODO use a string_view()
|
||||
colors.text(),
|
||||
ColorNone,
|
||||
gfx::Point(bounds.x, widget->textBaseline() - widget->textBlob()->baseline()),
|
||||
&delegate);
|
||||
}
|
||||
}
|
||||
int baselineAdjustment = widget->textBaseline();
|
||||
if (auto blob = widget->textBlob()) {
|
||||
baselineAdjustment -= blob->baseline();
|
||||
}
|
||||
else {
|
||||
text::FontMetrics metrics;
|
||||
widget->font()->metrics(&metrics);
|
||||
baselineAdjustment += metrics.ascent;
|
||||
}
|
||||
|
||||
bounds.x += delegate.textBounds().w;
|
||||
|
||||
// Draw suffix if there is enough space
|
||||
if (!widget->getSuffix().empty()) {
|
||||
Rect sufBounds(bounds.x,
|
||||
bounds.y,
|
||||
bounds.x2() - widget->childSpacing() - bounds.x,
|
||||
widget->textHeight());
|
||||
IntersectClip clip(g, sufBounds & widget->clientChildrenBounds());
|
||||
if (clip) {
|
||||
drawText(g,
|
||||
widget->getSuffix().c_str(),
|
||||
colors.entrySuffix(),
|
||||
ColorNone,
|
||||
widget,
|
||||
sufBounds,
|
||||
widget->align(),
|
||||
0);
|
||||
g->drawTextWithDelegate(std::string(pos, textString.end()), // TODO use a string_view()
|
||||
colors.text(),
|
||||
ColorNone,
|
||||
gfx::Point(bounds.x, baselineAdjustment),
|
||||
&delegate);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw caret at the end of the text
|
||||
if (!delegate.caretDrawn()) {
|
||||
bounds.x += delegate.textBounds().w;
|
||||
|
||||
gfx::Rect charBounds(bounds.x + widget->bounds().x,
|
||||
bounds.y + widget->bounds().y,
|
||||
0,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (c) 2020-2024 Igara Studio S.A.
|
||||
// Copyright (c) 2020-2025 Igara Studio S.A.
|
||||
// Copyright (c) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -250,6 +250,11 @@ void convert_image_to_surface(const doc::Image* image,
|
|||
os::SurfaceLock lockDst(surface);
|
||||
os::SurfaceFormatData fd;
|
||||
surface->getFormat(&fd);
|
||||
#if LAF_SKIA
|
||||
// Needed because Skia surfaces work with premultiplied alpha and
|
||||
// here we need to copy unpremultiplied alpha RGB values to the Skia surface.
|
||||
((os::SkiaSurface*)surface)->bitmap().setAlphaType(kUnpremul_SkAlphaType);
|
||||
#endif
|
||||
|
||||
switch (image->pixelFormat()) {
|
||||
case IMAGE_RGB:
|
||||
|
|
|
@ -60,7 +60,7 @@ private:
|
|||
|
||||
} // anonymous namespace
|
||||
|
||||
gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob)
|
||||
gfx::RectF get_text_blob_required_bounds(const text::TextBlobRef& blob)
|
||||
{
|
||||
ASSERT(blob != nullptr);
|
||||
|
||||
|
@ -74,32 +74,28 @@ gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob)
|
|||
bounds.w = 1;
|
||||
if (bounds.h < 1)
|
||||
bounds.h = 1;
|
||||
return gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h));
|
||||
return bounds;
|
||||
}
|
||||
|
||||
doc::ImageRef render_text_blob(const text::TextBlobRef& blob, gfx::Color color)
|
||||
doc::ImageRef render_text_blob(const text::TextBlobRef& blob,
|
||||
const gfx::RectF& textBounds,
|
||||
const ui::Paint& paint)
|
||||
{
|
||||
ASSERT(blob != nullptr);
|
||||
|
||||
os::Paint paint;
|
||||
// TODO offer Stroke, StrokeAndFill, and Fill styles
|
||||
paint.style(os::Paint::Fill);
|
||||
paint.color(color);
|
||||
|
||||
gfx::Size blobSize = get_text_blob_required_size(blob);
|
||||
|
||||
doc::ImageRef image(doc::Image::create(doc::IMAGE_RGB, blobSize.w, blobSize.h));
|
||||
doc::ImageRef image(
|
||||
doc::Image::create(doc::IMAGE_RGB, std::ceil(textBounds.w), std::ceil(textBounds.h)));
|
||||
|
||||
#ifdef LAF_SKIA
|
||||
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
|
||||
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
|
||||
text::draw_text(surface.get(), blob, gfx::PointF(0, 0), &paint);
|
||||
text::draw_text(surface.get(), blob, -textBounds.origin(), &paint);
|
||||
#endif // LAF_SKIA
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx::Color color)
|
||||
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, const ui::Paint& paint)
|
||||
{
|
||||
Fonts* fonts = Fonts::instance();
|
||||
ASSERT(fonts);
|
||||
|
@ -113,10 +109,6 @@ doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx
|
|||
const text::FontMgrRef fontMgr = fonts->fontMgr();
|
||||
ASSERT(fontMgr);
|
||||
|
||||
os::Paint paint;
|
||||
paint.style(os::Paint::StrokeAndFill);
|
||||
paint.color(color);
|
||||
|
||||
// We have to measure all text runs which might use different
|
||||
// fonts (e.g. if the given font is not enough to shape other code
|
||||
// points/languages).
|
||||
|
|
|
@ -11,25 +11,28 @@
|
|||
|
||||
#include "doc/image_ref.h"
|
||||
#include "gfx/color.h"
|
||||
#include "gfx/rect.h"
|
||||
#include "text/text_blob.h"
|
||||
#include "ui/paint.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
||||
class Color;
|
||||
class FontInfo;
|
||||
namespace skin {
|
||||
class SkinTheme;
|
||||
}
|
||||
|
||||
// Returns the exact bounds that are required to draw this TextBlob,
|
||||
// i.e. the image size that will be required in render_text_blob().
|
||||
gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob);
|
||||
// Returns the exact bounds that are required to draw this TextBlob in
|
||||
// the origin point (0, 0), i.e. the image size that will be required
|
||||
// in render_text_blob().
|
||||
gfx::RectF get_text_blob_required_bounds(const text::TextBlobRef& blob);
|
||||
|
||||
doc::ImageRef render_text_blob(const text::TextBlobRef& blob, gfx::Color color);
|
||||
doc::ImageRef render_text_blob(const text::TextBlobRef& blob,
|
||||
const gfx::RectF& textBounds,
|
||||
const ui::Paint& paint);
|
||||
|
||||
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx::Color color);
|
||||
doc::ImageRef render_text(const FontInfo& fontInfo,
|
||||
const std::string& text,
|
||||
const ui::Paint& paint);
|
||||
|
||||
} // namespace app
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
|
@ -257,11 +257,9 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem,
|
|||
((ExprEntry*)widget)->setDecimals(strtol(decimals, nullptr, 10));
|
||||
}
|
||||
if (elem_name == "filename") {
|
||||
const char* button_only = elem->Attribute("button_only");
|
||||
const app::FilenameField::Type type = ((button_only != nullptr &&
|
||||
strtol(button_only, nullptr, 10) == 1) ?
|
||||
app::FilenameField::Type::ButtonOnly :
|
||||
app::FilenameField::Type::EntryAndButton);
|
||||
const bool buttononly = bool_attr(elem, "buttononly", false);
|
||||
const app::FilenameField::Type type = (buttononly ? app::FilenameField::Type::ButtonOnly :
|
||||
app::FilenameField::Type::EntryAndButton);
|
||||
|
||||
widget = new app::FilenameField(type, "");
|
||||
}
|
||||
|
@ -534,7 +532,7 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem,
|
|||
}
|
||||
else if (elem_name == "font") {
|
||||
if (!widget)
|
||||
widget = new FontEntry;
|
||||
widget = new FontEntry(false);
|
||||
}
|
||||
|
||||
// Was the widget created?
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite Document Library
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2016 David Capello
|
||||
//
|
||||
// This file is released under the terms of the MIT license.
|
||||
|
@ -20,11 +20,12 @@
|
|||
#include "doc/primitives.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
|
||||
namespace doc {
|
||||
|
||||
static int generation = 0;
|
||||
static std::atomic<int> g_generation = 0;
|
||||
|
||||
Brush::Brush()
|
||||
{
|
||||
|
@ -300,7 +301,7 @@ void Brush::setCenter(const gfx::Point& center)
|
|||
// Cleans the brush's data (image and region).
|
||||
void Brush::clean()
|
||||
{
|
||||
m_gen = ++generation;
|
||||
m_gen = ++g_generation;
|
||||
m_image.reset();
|
||||
m_maskBitmap.reset();
|
||||
m_backupImage.reset();
|
||||
|
|
|
@ -106,7 +106,6 @@ void Box::onResize(ResizeEvent& ev)
|
|||
continue; \
|
||||
\
|
||||
int size = 0; \
|
||||
int sizeDiff = 0; \
|
||||
\
|
||||
if (align() & HOMOGENEOUS) { \
|
||||
if (i < visibleChildren - 1) \
|
||||
|
|
|
@ -128,6 +128,15 @@ int Entry::lastCaretPos() const
|
|||
return int(m_boxes.size() - 1);
|
||||
}
|
||||
|
||||
gfx::Point Entry::caretPosOnScreen() const
|
||||
{
|
||||
const gfx::Point caretPos = getCharBoxBounds(m_caret).point2();
|
||||
const os::Window* nativeWindow = display()->nativeWindow();
|
||||
const gfx::Point pos = nativeWindow->pointToScreen(caretPos + bounds().origin());
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
void Entry::setCaretPos(const int pos)
|
||||
{
|
||||
gfx::Size caretSize = theme()->getEntryCaretSize(this);
|
||||
|
@ -160,6 +169,8 @@ void Entry::setCaretPos(const int pos)
|
|||
startTimer();
|
||||
m_state = true;
|
||||
|
||||
os::System::instance()->setTextInput(true, caretPosOnScreen());
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
@ -251,7 +262,7 @@ gfx::Rect Entry::getEntryTextBounds() const
|
|||
return onGetEntryTextBounds();
|
||||
}
|
||||
|
||||
gfx::Rect Entry::getCharBoxBounds(const int i)
|
||||
gfx::Rect Entry::getCharBoxBounds(const int i) const
|
||||
{
|
||||
ASSERT(i >= 0 && i < int(m_boxes.size()));
|
||||
if (i >= 0 && i < int(m_boxes.size()))
|
||||
|
@ -288,8 +299,9 @@ bool Entry::onProcessMessage(Message* msg)
|
|||
}
|
||||
|
||||
// Start processing dead keys
|
||||
if (m_translate_dead_keys)
|
||||
os::System::instance()->setTranslateDeadKeys(true);
|
||||
if (m_translate_dead_keys) {
|
||||
os::System::instance()->setTextInput(true, caretPosOnScreen());
|
||||
}
|
||||
break;
|
||||
|
||||
case kFocusLeaveMessage:
|
||||
|
@ -304,7 +316,7 @@ bool Entry::onProcessMessage(Message* msg)
|
|||
|
||||
// Stop processing dead keys
|
||||
if (m_translate_dead_keys)
|
||||
os::System::instance()->setTranslateDeadKeys(false);
|
||||
os::System::instance()->setTextInput(false);
|
||||
break;
|
||||
|
||||
case kKeyDownMessage:
|
||||
|
|
|
@ -43,6 +43,7 @@ public:
|
|||
|
||||
int caretPos() const { return m_caret; }
|
||||
int lastCaretPos() const;
|
||||
gfx::Point caretPosOnScreen() const;
|
||||
|
||||
void setCaretPos(int pos);
|
||||
void setCaretToEnd();
|
||||
|
@ -76,7 +77,7 @@ public:
|
|||
obs::signal<void()> Change;
|
||||
|
||||
protected:
|
||||
gfx::Rect getCharBoxBounds(int i);
|
||||
gfx::Rect getCharBoxBounds(int i) const;
|
||||
|
||||
// Events
|
||||
bool onProcessMessage(Message* msg) override;
|
||||
|
|
|
@ -115,8 +115,8 @@ bool IntEntry::onProcessMessage(Message* msg)
|
|||
case kKeyDownMessage:
|
||||
if (hasFocus() && !isReadOnly()) {
|
||||
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
|
||||
int chr = keymsg->unicodeChar();
|
||||
if (chr >= 32 && (chr < '0' || chr > '9')) {
|
||||
const int chr = keymsg->unicodeChar();
|
||||
if (chr >= 32 && !onAcceptUnicodeChar(chr)) {
|
||||
// "Eat" all keys that aren't number
|
||||
return true;
|
||||
}
|
||||
|
@ -166,6 +166,11 @@ void IntEntry::onValueChange()
|
|||
// Do nothing
|
||||
}
|
||||
|
||||
bool IntEntry::onAcceptUnicodeChar(const int unicodeChar)
|
||||
{
|
||||
return (unicodeChar >= '0' && unicodeChar <= '9');
|
||||
}
|
||||
|
||||
void IntEntry::openPopup()
|
||||
{
|
||||
m_slider->setValue(getValue());
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Aseprite UI Library
|
||||
// Copyright (C) 2022 Igara Studio S.A.
|
||||
// Copyright (C) 2022-2025 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This file is released under the terms of the MIT license.
|
||||
|
@ -36,6 +36,7 @@ protected:
|
|||
|
||||
// New events
|
||||
virtual void onValueChange();
|
||||
virtual bool onAcceptUnicodeChar(int unicodeChar);
|
||||
|
||||
int m_min;
|
||||
int m_max;
|
||||
|
|
|
@ -515,37 +515,39 @@ void Theme::paintLayer(Graphics* g,
|
|||
if (!textBlob || style->font() != nullptr)
|
||||
textBlob = text::TextBlob::MakeWithShaper(m_fontMgr, g->font(), text);
|
||||
|
||||
const gfx::RectF blobSize = textBlob->bounds();
|
||||
const gfx::Border padding = style->padding();
|
||||
gfx::PointF pt;
|
||||
if (textBlob) {
|
||||
const gfx::RectF blobSize = textBlob->bounds();
|
||||
const gfx::Border padding = style->padding();
|
||||
gfx::PointF pt;
|
||||
|
||||
if (layer.align() & LEFT)
|
||||
pt.x = rc.x + padding.left();
|
||||
else if (layer.align() & RIGHT)
|
||||
pt.x = rc.x + rc.w - blobSize.w - padding.right();
|
||||
else
|
||||
pt.x = guiscaled_center(rc.x + padding.left(), rc.w - padding.width(), blobSize.w);
|
||||
if (layer.align() & LEFT)
|
||||
pt.x = rc.x + padding.left();
|
||||
else if (layer.align() & RIGHT)
|
||||
pt.x = rc.x + rc.w - blobSize.w - padding.right();
|
||||
else
|
||||
pt.x = guiscaled_center(rc.x + padding.left(), rc.w - padding.width(), blobSize.w);
|
||||
|
||||
if (layer.align() & TOP)
|
||||
pt.y = rc.y + padding.top();
|
||||
else if (layer.align() & BOTTOM)
|
||||
pt.y = rc.y + rc.h - blobSize.h - padding.bottom();
|
||||
else
|
||||
pt.y = baseline - textBlob->baseline();
|
||||
if (layer.align() & TOP)
|
||||
pt.y = rc.y + padding.top();
|
||||
else if (layer.align() & BOTTOM)
|
||||
pt.y = rc.y + rc.h - blobSize.h - padding.bottom();
|
||||
else
|
||||
pt.y = baseline - textBlob->baseline();
|
||||
|
||||
pt += layer.offset();
|
||||
pt += layer.offset();
|
||||
|
||||
Paint paint;
|
||||
if (gfx::geta(bgColor) > 0) { // Paint background
|
||||
paint.color(bgColor);
|
||||
paint.style(os::Paint::Fill);
|
||||
g->drawRect(gfx::RectF(textBlob->bounds()).offset(pt), paint);
|
||||
Paint paint;
|
||||
if (gfx::geta(bgColor) > 0) { // Paint background
|
||||
paint.color(bgColor);
|
||||
paint.style(os::Paint::Fill);
|
||||
g->drawRect(gfx::RectF(textBlob->bounds()).offset(pt), paint);
|
||||
}
|
||||
paint.color(layer.color());
|
||||
g->drawTextBlob(textBlob, gfx::PointF(pt), paint);
|
||||
|
||||
if (style->mnemonics() && mnemonic != 0)
|
||||
drawMnemonicUnderline(g, text, textBlob, pt, mnemonic, paint);
|
||||
}
|
||||
paint.color(layer.color());
|
||||
g->drawTextBlob(textBlob, gfx::PointF(pt), paint);
|
||||
|
||||
if (style->mnemonics() && mnemonic != 0)
|
||||
drawMnemonicUnderline(g, text, textBlob, pt, mnemonic, paint);
|
||||
}
|
||||
|
||||
if (style->font())
|
||||
|
|
Loading…
Reference in New Issue