Merge branch 'main' into beta

This commit is contained in:
David Capello 2022-04-25 11:45:31 -03:00
commit 2a59076f49
29 changed files with 365 additions and 251 deletions

2
laf

@ -1 +1 @@
Subproject commit d562448222c1990719f860246b5b3bcc3d2e1aa0
Subproject commit df53f4ac0cecada789bf84b4283947d2591833bd

View File

@ -209,7 +209,8 @@ class OptionsWindow : public app::gen::Options {
void uninstall() {
ASSERT(m_extension);
ASSERT(canBeUninstalled());
App::instance()->extensions().uninstallExtension(m_extension);
App::instance()->extensions().uninstallExtension(m_extension,
DeletePluginPref::kYes);
m_extension = nullptr;
}
@ -1546,7 +1547,7 @@ private:
// Uninstall old version
if (ext->canBeUninstalled()) {
exts.uninstallExtension(ext);
exts.uninstallExtension(ext, DeletePluginPref::kNo);
ExtensionItem* item = getItemByExtension(ext);
if (item)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -353,7 +353,7 @@ void Extension::enable(const bool state)
#endif // ENABLE_SCRIPTING
}
void Extension::uninstall()
void Extension::uninstall(const DeletePluginPref delPref)
{
if (!m_isInstalled)
return;
@ -366,14 +366,15 @@ void Extension::uninstall()
m_name.c_str(), m_path.c_str());
// Remove all files inside the extension path
uninstallFiles(m_path);
ASSERT(!base::is_directory(m_path));
uninstallFiles(m_path, delPref);
ASSERT(!base::is_directory(m_path) || delPref == DeletePluginPref::kNo);
m_isEnabled = false;
m_isInstalled = false;
}
void Extension::uninstallFiles(const std::string& path)
void Extension::uninstallFiles(const std::string& path,
const DeletePluginPref delPref)
{
#if 1 // Read the list of files to be uninstalled from __info.json file
@ -398,11 +399,17 @@ void Extension::uninstallFiles(const std::string& path)
}
}
// Delete __pref.lua file
// Delete __pref.lua file (only if specified, e.g. if the user is
// updating the extension, the preferences should be kept).
bool hasPrefFile = false;
{
std::string fn = base::join_path(path, kPrefLua);
if (base::is_file(fn))
base::delete_file(fn);
if (base::is_file(fn)) {
if (delPref == DeletePluginPref::kYes)
base::delete_file(fn);
else
hasPrefFile = true;
}
}
std::sort(installedDirs.begin(),
@ -427,7 +434,8 @@ void Extension::uninstallFiles(const std::string& path)
}
TRACE("EXT: Deleting extension directory '%s'\n", path.c_str());
base::remove_directory(path);
if (!hasPrefFile)
base::remove_directory(path);
#else // The following code delete the whole "path",
// we prefer the __info.json approach.
@ -439,7 +447,7 @@ void Extension::uninstallFiles(const std::string& path)
base::delete_file(fn);
}
else if (base::is_directory(fn)) {
uninstallFiles(fn);
uninstallFiles(fn, deleteUserPref);
}
}
@ -860,9 +868,10 @@ void Extensions::enableExtension(Extension* extension, const bool state)
generateExtensionSignals(extension);
}
void Extensions::uninstallExtension(Extension* extension)
void Extensions::uninstallExtension(Extension* extension,
const DeletePluginPref delPref)
{
extension->uninstall();
extension->uninstall(delPref);
generateExtensionSignals(extension);
auto it = std::find(m_extensions.begin(),

View File

@ -31,6 +31,8 @@ namespace app {
std::string commonPath;
};
enum DeletePluginPref { kNo, kYes };
class Extension {
friend class Extensions;
public:
@ -130,8 +132,9 @@ namespace app {
private:
void enable(const bool state);
void uninstall();
void uninstallFiles(const std::string& path);
void uninstall(const DeletePluginPref delPref);
void uninstallFiles(const std::string& path,
const DeletePluginPref delPref);
bool isDefaultTheme() const;
void updateCategory(const Category newCategory);
#ifdef ENABLE_SCRIPTING
@ -188,7 +191,8 @@ namespace app {
iterator end() { return m_extensions.end(); }
void enableExtension(Extension* extension, const bool state);
void uninstallExtension(Extension* extension);
void uninstallExtension(Extension* extension,
const DeletePluginPref delPref);
ExtensionInfo getCompressedExtensionInfo(const std::string& zipFn);
Extension* installCompressedExtension(const std::string& zipFn,
const ExtensionInfo& info);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -885,9 +885,6 @@ void FileOp::stop()
FileOp::~FileOp()
{
if (m_format)
m_format->destroyData(this);
delete m_seq.palette;
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -57,9 +58,4 @@ bool FileFormat::postLoad(FileOp* fop)
return onPostLoad(fop);
}
void FileFormat::destroyData(FileOp* fop)
{
onDestroyData(fop);
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -61,9 +61,6 @@ namespace app {
// Returns false cancelled the operation.
bool postLoad(FileOp* fop);
// Destroys the custom data stored in "fop->format_data" field.
void destroyData(FileOp* fop);
// Returns extra options for this format. It can return != NULL
// only if flags() returns FILE_SUPPORT_GET_FORMAT_OPTIONS.
FormatOptionsPtr askUserForFormatOptions(FileOp* fop) {
@ -86,7 +83,6 @@ namespace app {
#ifdef ENABLE_SAVE
virtual bool onSave(FileOp* fop) = 0;
#endif
virtual void onDestroyData(FileOp* fop) { }
virtual FormatOptionsPtr onAskUserForFormatOptions(FileOp* fop) {
return FormatOptionsPtr(nullptr);

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -9,9 +10,7 @@
#endif
#include "app/file/split_filename.h"
#include "base/convert_to.h"
#include "base/fs.h"
#include "base/string.h"
#include <cstring>
@ -20,41 +19,44 @@ namespace app {
// Splits a file-name like "my_ani0000.pcx" to "my_ani" and ".pcx",
// returning the number of the center; returns "-1" if the function
// can't split anything
int split_filename(const std::string& filename, std::string& left, std::string& right, int& width)
int split_filename(const std::string& filename,
std::string& left,
std::string& right,
int& width)
{
left = base::get_file_title_with_path(filename);
right = base::get_file_extension(filename);
if (!right.empty())
right.insert(right.begin(), '.');
// Remove all trailing numbers in the "left" side, and pass they to "result_str".
// Remove all trailing numbers in the "left" side.
std::string result_str;
width = 0;
for (;;) {
// Get the last UTF-8 character (as we don't have a
// reverse_iterator, we iterate from the beginning)
int chr = 0;
base::utf8_const_iterator begin(left.begin()), end(left.end());
base::utf8_const_iterator it(begin), prev(begin);
for (; it != end; prev=it, ++it)
chr = *it;
int num = -1;
int order = 1;
if ((chr >= '0') && (chr <= '9')) {
result_str.insert(result_str.begin(), chr);
width++;
auto it = left.rbegin();
auto end = left.rend();
left.erase(prev - begin);
while (it != end) {
const int chr = *it;
if (chr >= '0' && chr <= '9') {
if (num < 0)
num = 0;
num += order*(chr-'0');
order *= 10;
++width;
++it;
}
else
break;
}
// Convert the "buf" to integer and return it.
if (!result_str.empty()) {
return base::convert_to<int>(result_str);
}
else
return -1;
if (width > 0)
left.erase(left.end()-width, left.end());
return num;
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -12,7 +13,10 @@
namespace app {
int split_filename(const std::string& filename, std::string& left, std::string& right, int& width);
int split_filename(const std::string& filename,
std::string& left,
std::string& right,
int& width);
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -17,11 +18,21 @@ TEST(SplitFilename, Common)
std::string left, right;
int width;
EXPECT_EQ(-1, split_filename("sprite.png", left, right, width));
EXPECT_EQ("sprite", left);
EXPECT_EQ(".png", right);
EXPECT_EQ(0, width);
EXPECT_EQ(1, split_filename("C:\\test\\a1.png", left, right, width));
EXPECT_EQ("C:\\test\\a", left);
EXPECT_EQ(".png", right);
EXPECT_EQ(1, width);
EXPECT_EQ(2001, split_filename("/hi/bye2001.png", left, right, width));
EXPECT_EQ("/hi/bye", left);
EXPECT_EQ(".png", right);
EXPECT_EQ(4, width);
EXPECT_EQ(1, split_filename("C:/test/a1.png", left, right, width));
EXPECT_EQ("C:/test/a", left);
EXPECT_EQ(".png", right);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -71,7 +71,7 @@ unsigned int current_file_system_version = 0;
#endif
// a position in the file-system
class FileItem : public IFileItem {
class FileItem final : public IFileItem {
public:
// TODO make all these fields private
std::string m_keyname;
@ -123,6 +123,13 @@ public:
m_thumbnailProgress = progress;
}
bool needThumbnail() const override {
return
!isBrowsable() &&
m_thumbnail == nullptr &&
m_thumbnailProgress < 1.0;
}
os::SurfaceRef getThumbnail() override;
void setThumbnail(const os::SurfaceRef& thumbnail) override;
@ -593,6 +600,8 @@ os::SurfaceRef FileItem::getThumbnail()
void FileItem::setThumbnail(const os::SurfaceRef& newThumbnail)
{
m_thumbnailProgress = 1.0;
if (newThumbnail)
newThumbnail->ref();
auto old = m_thumbnail.exchange(newThumbnail.get());

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -87,8 +87,10 @@ namespace app {
virtual double getThumbnailProgress() = 0;
virtual void setThumbnailProgress(double progress) = 0;
virtual bool needThumbnail() const = 0;
virtual os::SurfaceRef getThumbnail() = 0;
virtual void setThumbnail(const os::SurfaceRef& thumbnail) = 0;
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -18,7 +18,6 @@
#include "app/file_system.h"
#include "app/util/conversion_to_surface.h"
#include "base/clamp.h"
#include "base/scoped_lock.h"
#include "base/thread.h"
#include "doc/algorithm/rotate.h"
#include "doc/image.h"
@ -28,6 +27,7 @@
#include "os/system.h"
#include "render/projection.h"
#include "render/render.h"
#include "ui/system.h"
#include <algorithm>
#include <atomic>
@ -50,7 +50,7 @@ public:
~Worker() {
{
base::scoped_lock lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
if (m_fop)
m_fop->stop();
}
@ -58,7 +58,7 @@ public:
}
void stop() const {
base::scoped_lock lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
if (m_fop)
m_fop->stop();
}
@ -68,7 +68,7 @@ public:
}
void updateProgress() {
base::scoped_lock lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
if (m_item.fileitem && m_item.fop) {
double progress = m_item.fop->progress();
if (progress > m_item.fileitem->getThumbnailProgress())
@ -81,7 +81,7 @@ private:
ASSERT(!m_fop);
try {
{
base::scoped_lock lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
m_fop = m_item.fop;
ASSERT(m_fop);
}
@ -168,7 +168,7 @@ private:
0, 0, 0, 0, thumbnailImage->width(), thumbnailImage->height());
{
base::scoped_lock lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
m_item.fileitem->setThumbnail(thumbnail);
}
}
@ -176,20 +176,29 @@ private:
THUMB_TRACE("FOP done with thumbnail: %s %s\n",
m_item.fileitem->fileName().c_str(),
(m_fop->isStop() ? " (stop)": ""));
// Reset the m_item (first the fileitem so this worker is not
// associated to this fileitem anymore, and then the FileOp).
{
base::scoped_lock lock(m_mutex);
m_item.fileitem = nullptr;
}
}
catch (const std::exception& e) {
m_fop->setError("Error loading file:\n%s", e.what());
}
if (!m_fop->isStop()) {
// Set a nullptr thumbnail if we failed loading the given file,
// in this way we're not going to re-try generating this same
// thumbnail.
if (m_item.fileitem->needThumbnail())
m_item.fileitem->setThumbnail(nullptr);
}
// Reset the m_item (first the fileitem so this worker is not
// associated to this fileitem anymore, and then the FileOp).
{
std::lock_guard<std::mutex> lock(m_mutex);
m_item.fileitem = nullptr;
}
m_fop->done();
{
base::scoped_lock lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
m_item.fop = nullptr;
delete m_fop;
m_fop = nullptr;
@ -199,8 +208,14 @@ private:
void loadBgThread() {
while (!m_queue.empty()) {
while (m_queue.try_pop(m_item)) {
loadItem();
bool success = true;
while (success) {
{
std::lock_guard<std::mutex> lock(m_mutex); // To access m_item
success = m_queue.try_pop(m_item);
}
if (success)
loadItem();
}
base::this_thread::yield();
}
@ -210,24 +225,22 @@ private:
base::concurrent_queue<Item>& m_queue;
app::ThumbnailGenerator::Item m_item;
FileOp* m_fop;
mutable base::mutex m_mutex;
mutable std::mutex m_mutex;
std::atomic<bool> m_isDone;
base::thread m_thread;
std::thread m_thread;
};
static void delete_singleton(ThumbnailGenerator* singleton)
{
delete singleton;
}
ThumbnailGenerator* ThumbnailGenerator::instance()
{
static ThumbnailGenerator* singleton = nullptr;
if (singleton == NULL) {
singleton = new ThumbnailGenerator();
App::instance()->Exit.connect([&]{ delete_singleton(singleton); });
static std::unique_ptr<ThumbnailGenerator> singleton;
ui::assert_ui_thread();
if (!singleton) {
// We cannot use std::make_unique() because ThumbnailGenerator
// ctor is private.
singleton.reset(new ThumbnailGenerator);
App::instance()->Exit.connect([&]{ singleton.reset(); });
}
return singleton;
return singleton.get();
}
ThumbnailGenerator::ThumbnailGenerator()
@ -239,15 +252,13 @@ ThumbnailGenerator::ThumbnailGenerator()
bool ThumbnailGenerator::checkWorkers()
{
base::scoped_lock hold(m_workersAccess);
std::lock_guard<std::mutex> hold(m_workersAccess);
bool doingWork = (!m_workers.empty());
for (WorkerList::iterator
it=m_workers.begin(); it != m_workers.end(); ) {
Worker* worker = *it;
worker->updateProgress();
if (worker->isDone()) {
delete worker;
(*it)->updateProgress();
if ((*it)->isDone()) {
it = m_workers.erase(it);
}
else {
@ -260,8 +271,7 @@ bool ThumbnailGenerator::checkWorkers()
void ThumbnailGenerator::generateThumbnail(IFileItem* fileitem)
{
if (fileitem->isBrowsable() ||
fileitem->getThumbnail())
if (!fileitem->needThumbnail())
return;
if (fileitem->getThumbnailProgress() > 0.0) {
@ -300,11 +310,12 @@ void ThumbnailGenerator::generateThumbnail(IFileItem* fileitem)
fileitem->fileName().c_str(),
FILE_LOAD_SEQUENCE_NONE |
FILE_LOAD_ONE_FRAME));
if (!fop)
return;
if (fop->hasError())
if (!fop || fop->hasError()) {
// Set a nullptr thumbnail so we don't try to generate a thumbnail
// for this fileitem again.
fileitem->setThumbnail(nullptr);
return;
}
m_remainingItems.push(Item(fileitem, fop.get()));
fop.release();
@ -327,18 +338,16 @@ void ThumbnailGenerator::stopAllWorkers()
}
}
base::scoped_lock hold(m_workersAccess);
for (auto worker : m_workers)
std::lock_guard<std::mutex> hold(m_workersAccess);
for (const auto& worker : m_workers)
worker->stop();
}
void ThumbnailGenerator::startWorker()
{
base::scoped_lock hold(m_workersAccess);
std::lock_guard<std::mutex> hold(m_workersAccess);
if (m_workers.size() < m_maxWorkers) {
std::unique_ptr<Worker> worker(new Worker(m_remainingItems));
m_workers.push_back(worker.get());
worker.release();
m_workers.push_back(std::make_unique<Worker>(m_remainingItems));
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,9 +10,9 @@
#pragma once
#include "base/concurrent_queue.h"
#include "base/mutex.h"
#include <memory>
#include <mutex>
#include <vector>
namespace base {
@ -47,7 +47,8 @@ namespace app {
void startWorker();
class Worker;
typedef std::vector<Worker*> WorkerList;
using WorkerPtr = std::unique_ptr<Worker>;
using WorkerList = std::vector<WorkerPtr>;
struct Item {
IFileItem* fileitem;
@ -61,8 +62,7 @@ namespace app {
int m_maxWorkers;
WorkerList m_workers;
base::mutex m_workersAccess;
std::unique_ptr<base::thread> m_stopThread;
std::mutex m_workersAccess;
base::concurrent_queue<Item> m_remainingItems;
};

View File

@ -885,7 +885,7 @@ void FileList::selectIndex(int index)
void FileList::generateThumbnailForFileItem(IFileItem* fi)
{
if (fi && animation() == ANI_NONE) {
if (fi && fi->needThumbnail() && animation() == ANI_NONE) {
auto it = std::find(m_generateThumbnailsForTheseItems.begin(),
m_generateThumbnailsForTheseItems.end(), fi);
if (it != m_generateThumbnailsForTheseItems.end())

View File

@ -29,6 +29,7 @@
#include "base/fs.h"
#include "base/log.h"
#include "base/string.h"
#include "base/utf8_decode.h"
#include "gfx/border.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -1131,14 +1132,13 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
int scroll = delegate.index();
const std::string& textString = widget->text();
base::utf8_const_iterator utf8_it((textString.begin()));
int textlen = base::utf8_length(textString);
scroll = std::min(scroll, textlen);
if (scroll)
utf8_it += scroll;
base::utf8_decode dec(textString);
auto pos = dec.pos();
for (int i=0; i<scroll && dec.next(); ++i)
pos = dec.pos();
g->drawText(utf8_it,
base::utf8_const_iterator(textString.end()),
// TODO use a string_view()
g->drawText(std::string(pos, textString.end()),
colors.text(), ColorNone,
bounds.origin(), &delegate);

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -47,52 +48,49 @@ doc::Image* render_text(const std::string& fontfile, int fontsize,
image.reset(doc::Image::create(doc::IMAGE_RGB, bounds.w, bounds.h));
doc::clear_image(image.get(), 0);
ft::ForEachGlyph<ft::Face> feg(face);
if (feg.initialize(base::utf8_const_iterator(text.begin()),
base::utf8_const_iterator(text.end()))) {
do {
auto glyph = feg.glyph();
if (!glyph)
continue;
ft::ForEachGlyph<ft::Face> feg(face, text);
while (feg.next()) {
auto glyph = feg.glyph();
if (!glyph)
continue;
int t, yimg = - bounds.y + int(glyph->y);
int t, yimg = - bounds.y + int(glyph->y);
for (int v=0; v<int(glyph->bitmap->rows); ++v, ++yimg) {
const uint8_t* p = glyph->bitmap->buffer + v*glyph->bitmap->pitch;
int ximg = - bounds.x + int(glyph->x);
int bit = 0;
for (int v=0; v<int(glyph->bitmap->rows); ++v, ++yimg) {
const uint8_t* p = glyph->bitmap->buffer + v*glyph->bitmap->pitch;
int ximg = - bounds.x + int(glyph->x);
int bit = 0;
for (int u=0; u<int(glyph->bitmap->width); ++u, ++ximg) {
int alpha;
for (int u=0; u<int(glyph->bitmap->width); ++u, ++ximg) {
int alpha;
if (antialias) {
alpha = *(p++);
}
else {
alpha = ((*p) & (1 << (7 - (bit++))) ? 255: 0);
if (bit == 8) {
bit = 0;
++p;
}
}
int output_alpha = MUL_UN8(doc::rgba_geta(color), alpha, t);
if (output_alpha) {
doc::color_t output_color =
doc::rgba(doc::rgba_getr(color),
doc::rgba_getg(color),
doc::rgba_getb(color),
output_alpha);
doc::put_pixel(
image.get(), ximg, yimg,
doc::rgba_blender_normal(
doc::get_pixel(image.get(), ximg, yimg),
output_color));
if (antialias) {
alpha = *(p++);
}
else {
alpha = ((*p) & (1 << (7 - (bit++))) ? 255: 0);
if (bit == 8) {
bit = 0;
++p;
}
}
int output_alpha = MUL_UN8(doc::rgba_geta(color), alpha, t);
if (output_alpha) {
doc::color_t output_color =
doc::rgba(doc::rgba_getr(color),
doc::rgba_getg(color),
doc::rgba_getb(color),
output_alpha);
doc::put_pixel(
image.get(), ximg, yimg,
doc::rgba_blender_normal(
doc::get_pixel(image.get(), ximg, yimg),
output_color));
}
}
} while (feg.nextChar());
}
}
}
else {

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020-2021 Igara Studio S.A.
// Copyright (c) 2020-2022 Igara Studio S.A.
// Copyright (c) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -304,7 +304,7 @@ static uint32_t* col_diff_r;
static uint32_t* col_diff_b;
static uint32_t* col_diff_a;
static void initBestfit()
void Palette::initBestfit()
{
col_diff.resize(4*128, 0);
col_diff_g = &col_diff[128*0];
@ -327,9 +327,7 @@ int Palette::findBestfit(int r, int g, int b, int a, int mask_index) const
ASSERT(g >= 0 && g <= 255);
ASSERT(b >= 0 && b <= 255);
ASSERT(a >= 0 && a <= 255);
if (col_diff.empty())
initBestfit();
ASSERT(!col_diff.empty());
r >>= 3;
g >>= 3;

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -24,6 +24,8 @@ namespace doc {
class Palette : public Object {
public:
static void initBestfit();
Palette();
Palette(frame_t frame, int ncolors);
Palette(const Palette& palette);

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2020-2022 Igara Studio S.A.
// Copyright (c) 2001-2015 David Capello
//
// This file is released under the terms of the MIT license.
@ -166,6 +166,9 @@ TEST(Remap, BetweenPalettesDontChangeMaskForced)
TEST(Remap, BetweenPalettesNonInvertible)
{
// create_remap_to_change_palette() uses findBestfit()
doc::Palette::initBestfit();
Palette a(frame_t(0), 4);
Palette b(frame_t(0), 3);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -17,6 +17,7 @@
#include "base/exception.h"
#include "base/memory.h"
#include "base/system_console.h"
#include "doc/palette.h"
#include "os/error.h"
#include "os/system.h"
#include "ver/info.h"
@ -93,6 +94,7 @@ int app_main(int argc, char* argv[])
base::SystemConsole systemConsole;
app::AppOptions options(argc, const_cast<const char**>(argv));
os::SystemRef system(os::make_system());
doc::Palette::initBestfit();
app::App app;
#if ENABLE_SENTRY

View File

@ -1,4 +1,4 @@
Copyright (c) 2018-2021 Igara Studio S.A.
Copyright (c) 2018-2022 Igara Studio S.A.
Copyright (c) 2001-2018 David Capello
Permission is hereby granted, free of charge, to any person obtaining

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -23,18 +23,26 @@
#include "ui/size_hint_event.h"
#include "ui/system.h"
#include "ui/theme.h"
#include "ui/timer.h"
#include "ui/widget.h"
#include <algorithm>
#include <cctype>
#include <cstdarg>
#include <cstdio>
#include <memory>
namespace ui {
// Shared timer between all entries.
static std::unique_ptr<Timer> s_timer;
static inline bool is_word_char(int ch) {
return (ch && !std::isspace(ch) && !std::ispunct(ch));
}
Entry::Entry(const int maxsize, const char* format, ...)
: Widget(kEntryWidget)
, m_timer(500, this)
, m_maxsize(maxsize)
, m_caret(0)
, m_scroll(0)
@ -71,6 +79,7 @@ Entry::Entry(const int maxsize, const char* format, ...)
Entry::~Entry()
{
stopTimer();
}
void Entry::setMaxTextLength(const int maxsize)
@ -92,14 +101,14 @@ void Entry::showCaret()
{
m_hidden = false;
if (shouldStartTimer(hasFocus()))
m_timer.start();
startTimer();
invalidate();
}
void Entry::hideCaret()
{
m_hidden = true;
m_timer.stop();
stopTimer();
invalidate();
}
@ -135,7 +144,7 @@ void Entry::setCaretPos(int pos)
}
if (shouldStartTimer(hasFocus()))
m_timer.start();
startTimer();
m_state = true;
invalidate();
@ -195,10 +204,18 @@ Entry::Range Entry::selectedRange() const
void Entry::setSuffix(const std::string& suffix)
{
if (m_suffix != suffix) {
m_suffix = suffix;
invalidate();
}
// No-op cases
if ((!m_suffix && suffix.empty()) ||
(m_suffix && *m_suffix == suffix))
return;
m_suffix = std::make_unique<std::string>(suffix);
invalidate();
}
std::string Entry::getSuffix()
{
return (m_suffix ? *m_suffix: std::string());
}
void Entry::setTranslateDeadKeys(bool state)
@ -224,16 +241,16 @@ bool Entry::onProcessMessage(Message* msg)
switch (msg->type()) {
case kTimerMessage:
if (hasFocus() && static_cast<TimerMessage*>(msg)->timer() == &m_timer) {
if (hasFocus() && static_cast<TimerMessage*>(msg)->timer() == s_timer.get()) {
// Blinking caret
m_state = m_state ? false: true;
m_state = (m_state ? false: true);
invalidate();
}
break;
case kFocusEnterMessage:
if (shouldStartTimer(true))
m_timer.start();
startTimer();
m_state = true;
invalidate();
@ -254,7 +271,7 @@ bool Entry::onProcessMessage(Message* msg)
case kFocusLeaveMessage:
invalidate();
m_timer.stop();
stopTimer();
if (!m_lock_selection)
deselectText();
@ -374,6 +391,11 @@ bool Entry::onProcessMessage(Message* msg)
case kMouseDownMessage:
captureMouse();
// Disable selecting words if we click again (only
// double-clicking enables selecting words again).
if (!m_selecting_words.isEmpty())
m_selecting_words.reset();
case kMouseMoveMessage:
if (hasCapture()) {
bool is_dirty = false;
@ -392,14 +414,28 @@ bool Entry::onProcessMessage(Message* msg)
m_recent_focused = false;
m_select = m_caret;
}
else if (msg->type() == kMouseDownMessage)
// Deselect
else if (msg->type() == kMouseDownMessage) {
m_select = m_caret;
}
// Continue selecting words
else if (!m_selecting_words.isEmpty()) {
Range toWord = wordRange(m_caret);
if (toWord.from < m_selecting_words.from) {
m_select = std::max(m_selecting_words.to, toWord.to);
setCaretPos(std::min(m_selecting_words.from, toWord.from));
}
else {
m_select = std::min(m_selecting_words.from, toWord.from);
setCaretPos(std::max(m_selecting_words.to, toWord.to));
}
}
}
// Show the caret
if (is_dirty) {
if (shouldStartTimer(true))
m_timer.start();
startTimer();
m_state = true;
}
@ -411,6 +447,9 @@ bool Entry::onProcessMessage(Message* msg)
if (hasCapture()) {
releaseMouse();
if (!m_selecting_words.isEmpty())
m_selecting_words.reset();
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
if (mouseMsg->right()) {
// This flag is disabled in kFocusEnterMessage message handler.
@ -426,10 +465,11 @@ bool Entry::onProcessMessage(Message* msg)
if (!hasFocus())
requestFocus();
forwardWord();
m_select = m_caret;
backwardWord();
invalidate();
m_selecting_words = wordRange(m_caret);
selectText(m_selecting_words.from, m_selecting_words.to);
// Capture mouse to continue selecting words on kMouseMoveMessage
captureMouse();
return true;
case kMouseEnterMessage:
@ -750,43 +790,30 @@ void Entry::executeCmd(EntryCmd cmd, int unicodeChar, bool shift_pressed)
invalidate();
}
#define IS_WORD_CHAR(ch) \
(!((!ch) || (std::isspace(ch)) || \
((ch) == '/') || ((ch) == '\\')))
void Entry::forwardWord()
{
int textlen = lastCaretPos();
int ch;
for (; m_caret < textlen; ++m_caret) {
ch = m_boxes[m_caret].codepoint;
if (IS_WORD_CHAR(ch))
if (is_word_char(m_boxes[m_caret].codepoint))
break;
}
for (; m_caret < textlen; ++m_caret) {
ch = m_boxes[m_caret].codepoint;
if (!IS_WORD_CHAR(ch)) {
++m_caret;
if (!is_word_char(m_boxes[m_caret].codepoint))
break;
}
}
}
void Entry::backwardWord()
{
int ch;
for (--m_caret; m_caret >= 0; --m_caret) {
ch = m_boxes[m_caret].codepoint;
if (IS_WORD_CHAR(ch))
if (is_word_char(m_boxes[m_caret].codepoint))
break;
}
for (; m_caret >= 0; --m_caret) {
ch = m_boxes[m_caret].codepoint;
if (!IS_WORD_CHAR(ch)) {
if (!is_word_char(m_boxes[m_caret].codepoint)) {
++m_caret;
break;
}
@ -796,6 +823,41 @@ void Entry::backwardWord()
m_caret = 0;
}
Entry::Range Entry::wordRange(int pos)
{
const int last = lastCaretPos();
pos = base::clamp(pos, 0, last);
int i, j;
i = j = pos;
// Select word space
if (is_word_char(m_boxes[pos].codepoint)) {
for (; i>=0; --i) {
if (!is_word_char(m_boxes[i].codepoint))
break;
}
++i;
for (; j<=last; ++j) {
if (!is_word_char(m_boxes[j].codepoint))
break;
}
}
// Select punctuation space
else {
for (; i>=0; --i) {
if (is_word_char(m_boxes[i].codepoint))
break;
}
++i;
for (; j<=last; ++j) {
if (is_word_char(m_boxes[j].codepoint))
break;
}
}
return Range(i, j);
}
bool Entry::isPosInSelection(int pos)
{
return (pos >= std::min(m_caret, m_select) &&
@ -863,9 +925,7 @@ void Entry::recalcCharBoxes(const std::string& text)
{
int lastTextIndex = int(text.size());
CalcBoxesTextDelegate delegate(lastTextIndex);
os::draw_text(nullptr, font(),
base::utf8_const_iterator(text.begin()),
base::utf8_const_iterator(text.end()),
os::draw_text(nullptr, font(), text,
gfx::ColorNone, gfx::ColorNone, 0, 0, &delegate);
m_boxes = delegate.boxes();
@ -892,4 +952,20 @@ void Entry::deleteRange(const Range& range, std::string& text)
m_caret = range.from;
}
void Entry::startTimer()
{
if (s_timer)
s_timer->stop();
s_timer = std::make_unique<Timer>(500, this);
s_timer->start();
}
void Entry::stopTimer()
{
if (s_timer) {
s_timer->stop();
s_timer.reset();
}
}
} // namespace ui

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -10,9 +10,10 @@
#pragma once
#include "obs/signal.h"
#include "ui/timer.h"
#include "ui/widget.h"
#include <memory>
namespace ui {
class MouseMessage;
@ -21,8 +22,11 @@ namespace ui {
public:
struct Range {
int from = -1, to = -1;
Range() { }
Range(int from, int to) : from(from), to(to) { }
bool isEmpty() const { return from < 0; }
int size() const { return to - from; }
void reset() { from = to = -1; }
};
Entry(const int maxsize, const char *format, ...);
@ -49,7 +53,7 @@ namespace ui {
Range selectedRange() const;
void setSuffix(const std::string& suffix);
const std::string& getSuffix() { return m_suffix; }
std::string getSuffix();
void setTranslateDeadKeys(bool state);
@ -98,11 +102,14 @@ namespace ui {
void executeCmd(EntryCmd cmd, int ascii, bool shift_pressed);
void forwardWord();
void backwardWord();
Range wordRange(int pos);
bool isPosInSelection(int pos);
void showEditPopupMenu(const gfx::Point& pt);
void recalcCharBoxes(const std::string& text);
bool shouldStartTimer(const bool hasFocus);
void deleteRange(const Range& range, std::string& text);
void startTimer();
void stopTimer();
class CalcBoxesTextDelegate;
@ -113,21 +120,21 @@ namespace ui {
CharBox() { codepoint = from = to = width = 0; }
};
typedef std::vector<CharBox> CharBoxes;
using CharBoxes = std::vector<CharBox>;
CharBoxes m_boxes;
Timer m_timer;
int m_maxsize;
int m_caret;
int m_scroll;
int m_select;
bool m_hidden;
bool m_state; // show or not the text caret
bool m_readonly;
bool m_recent_focused;
bool m_lock_selection;
bool m_translate_dead_keys;
std::string m_suffix;
bool m_hidden : 1;
bool m_state : 1; // show or not the text caret
bool m_readonly : 1;
bool m_recent_focused : 1;
bool m_lock_selection : 1;
bool m_translate_dead_keys : 1;
Range m_selecting_words;
std::unique_ptr<std::string> m_suffix;
};
} // namespace ui

View File

@ -373,8 +373,7 @@ void Graphics::setFont(const os::FontRef& font)
m_font = font;
}
void Graphics::drawText(base::utf8_const_iterator it,
const base::utf8_const_iterator& end,
void Graphics::drawText(const std::string& str,
gfx::Color fg, gfx::Color bg,
const gfx::Point& origPt,
os::DrawTextDelegate* delegate)
@ -383,18 +382,11 @@ void Graphics::drawText(base::utf8_const_iterator it,
os::SurfaceLock lock(m_surface.get());
gfx::Rect textBounds =
os::draw_text(m_surface.get(), m_font.get(), it, end, fg, bg, pt.x, pt.y, delegate);
os::draw_text(m_surface.get(), m_font.get(), str, fg, bg, pt.x, pt.y, delegate);
dirty(gfx::Rect(pt.x, pt.y, textBounds.w, textBounds.h));
}
void Graphics::drawText(const std::string& str, gfx::Color fg, gfx::Color bg, const gfx::Point& pt)
{
drawText(base::utf8_const_iterator(str.begin()),
base::utf8_const_iterator(str.end()),
fg, bg, pt, nullptr);
}
namespace {
class DrawUITextDelegate : public os::DrawTextDelegate {
@ -468,10 +460,8 @@ void Graphics::drawUIText(const std::string& str, gfx::Color fg, gfx::Color bg,
int y = m_dy+pt.y;
DrawUITextDelegate delegate(m_surface.get(), m_font.get(), mnemonic);
os::draw_text(m_surface.get(), m_font.get(),
base::utf8_const_iterator(str.begin()),
base::utf8_const_iterator(str.end()),
fg, bg, x, y, &delegate);
os::draw_text(m_surface.get(), m_font.get(), str,
fg, bg, x, y, &delegate);
dirty(delegate.bounds());
}
@ -493,11 +483,9 @@ gfx::Size Graphics::measureUIText(const std::string& str)
int Graphics::measureUITextLength(const std::string& str, os::Font* font)
{
DrawUITextDelegate delegate(nullptr, font, 0);
os::draw_text(nullptr, font,
base::utf8_const_iterator(str.begin()),
base::utf8_const_iterator(str.end()),
gfx::ColorNone, gfx::ColorNone, 0, 0,
&delegate);
os::draw_text(nullptr, font, str,
gfx::ColorNone, gfx::ColorNone, 0, 0,
&delegate);
return delegate.bounds().w;
}

View File

@ -120,11 +120,10 @@ namespace ui {
os::Font* font() { return m_font.get(); }
void setFont(const os::FontRef& font);
void drawText(base::utf8_const_iterator it,
const base::utf8_const_iterator& end,
gfx::Color fg, gfx::Color bg, const gfx::Point& pt,
os::DrawTextDelegate* delegate);
void drawText(const std::string& str, gfx::Color fg, gfx::Color bg, const gfx::Point& pt);
void drawText(const std::string& str,
gfx::Color fg, gfx::Color bg,
const gfx::Point& pt,
os::DrawTextDelegate* delegate = nullptr);
void drawUIText(const std::string& str, gfx::Color fg, gfx::Color bg, const gfx::Point& pt, const int mnemonic);
void drawAlignedUIText(const std::string& str, gfx::Color fg, gfx::Color bg, const gfx::Rect& rc, const int align);

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -43,7 +43,7 @@ namespace ui {
protected:
virtual void onTick();
public:
private:
Widget* m_owner;
int m_interval;
bool m_running;

View File

@ -16,6 +16,7 @@
#include "base/clamp.h"
#include "base/memory.h"
#include "base/string.h"
#include "base/utf8_decode.h"
#include "os/font.h"
#include "os/surface.h"
#include "os/system.h"
@ -1461,19 +1462,18 @@ void Widget::processMnemonicFromText(int escapeChar)
if (!m_text.empty())
newText.reserve(m_text.size());
for (base::utf8_const_iterator
it(m_text.begin()),
end(m_text.end()); it != end; ++it) {
if (*it == escapeChar) {
++it;
if (it == end) {
base::utf8_decode decode(m_text);
while (int chr = decode.next()) {
if (chr == escapeChar) {
chr = decode.next();
if (!chr) {
break; // Ill-formed string (it ends with escape character)
}
else if (*it != escapeChar) {
setMnemonic(*it);
else if (chr != escapeChar) {
setMnemonic(chr);
}
}
newText.push_back(*it);
newText.push_back(chr);
}
setText(base::to_utf8(newText));

View File

@ -132,6 +132,7 @@ set(ENABLE_LIBXML2 OFF CACHE BOOL "Enable the use of the system libxml2 library
set(ENABLE_CAT OFF CACHE BOOL "Enable cat building")
set(ENABLE_TAR OFF CACHE BOOL "Enable tar building")
set(ENABLE_CPIO OFF CACHE BOOL "Enable cpio building")
set(ENABLE_LIBB2 OFF CACHE BOOL "Enable the use of the system LIBB2 library if found")
add_subdirectory(libarchive)
target_include_directories(archive_static INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/libarchive/libarchive>)