mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-17 13:20:45 +00:00
Merge branch 'main' into beta
This commit is contained in:
commit
f62b5eafb1
32
.pre-commit-config.yaml
Normal file
32
.pre-commit-config.yaml
Normal file
@ -0,0 +1,32 @@
|
||||
exclude: 'third_party/.*|laf/.*'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
args: [--whitespaces-count, '8']
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v19.1.5
|
||||
hooks:
|
||||
- id: clang-format
|
||||
files: \.(c|h|cpp|hpp|cc|hh|cxx|hxx)$
|
||||
types_or: [text]
|
||||
|
||||
- repo: https://github.com/pocc/pre-commit-hooks
|
||||
rev: v1.3.5
|
||||
hooks:
|
||||
- id: clang-tidy
|
||||
files: \.(c|h|cpp|hpp|cc|hh|cxx|hxx)$
|
||||
args: [--fix, --quiet, --use-color]
|
||||
types_or: [text]
|
||||
additional_dependencies: [clang-tidy==19.1.0]
|
||||
require_serial: true
|
||||
stages: [manual]
|
@ -16,7 +16,7 @@ Before you submit an issue:
|
||||
click the `Subscribe` or `Watching` button to get notifications
|
||||
via email.
|
||||
|
||||
# Compilation problem
|
||||
# Compilation problems
|
||||
|
||||
Before you submit an issue or a post about a **compilation problem**,
|
||||
check the following items:
|
||||
@ -77,6 +77,20 @@ new [features](https://community.aseprite.org/c/features),
|
||||
[bug reports](https://community.aseprite.org/c/bugs), etc. You are
|
||||
encouraged to create mockups for any issue you see and attach them.
|
||||
|
||||
## Pre-commit hooks
|
||||
|
||||
We use [pre-commit](https://pre-commit.com/) as way to set up hooks, you can install it with:
|
||||
|
||||
```
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
If you need to run it manually, use `pre-commit run`.
|
||||
|
||||
To run `clang-tidy`, you can use `pre-commit run --hook-stage manual clang-tidy`.
|
||||
Make sure to check the suggestions and to not apply them arbitrarily, since some might not be 100% applicable to what you're doing.
|
||||
|
||||
## Code submission policy
|
||||
|
||||
We have some rules for the changes and commits that are contributed:
|
||||
|
@ -59,7 +59,7 @@ To compile Aseprite you will need:
|
||||
|
||||
## Windows dependencies
|
||||
|
||||
* Windows 10 (we don't support cross-compiling)
|
||||
* Windows 10/11 (we don't support cross-compiling)
|
||||
* [Visual Studio Community 2022](https://visualstudio.microsoft.com/downloads/) (we don't support [MinGW](#mingw))
|
||||
* The [Desktop development with C++ item + Windows 10.0.18362.0 SDK](https://imgur.com/a/7zs51IT)
|
||||
from the Visual Studio installer
|
||||
|
@ -859,6 +859,8 @@ x = X:
|
||||
y = Y:
|
||||
width = Width:
|
||||
height = Height:
|
||||
columns = Columns:
|
||||
rows = Rows:
|
||||
padding = Padding
|
||||
horizontal_padding = Horizontal:
|
||||
vertical_padding = Vertical:
|
||||
|
@ -24,6 +24,14 @@
|
||||
<label text="@.height" />
|
||||
<expr id="height" text="16" />
|
||||
|
||||
<separator horizontal="true" cell_hspan="4" />
|
||||
|
||||
<label text="@.columns" />
|
||||
<expr id="columns" />
|
||||
|
||||
<label text="@.rows" />
|
||||
<expr id="rows" />
|
||||
|
||||
<check id="padding_enabled" text="@.padding" cell_hspan="4" />
|
||||
|
||||
<label text="@.horizontal_padding" id="horizontal_padding_label" />
|
||||
|
2
laf
2
laf
@ -1 +1 @@
|
||||
Subproject commit 4766fd95801af52b904146e79e5d89a916de49de
|
||||
Subproject commit 3eee03a1f3a3ce7b06e93b1106fc356bc0307c42
|
@ -44,18 +44,41 @@ namespace app {
|
||||
|
||||
using namespace ui;
|
||||
|
||||
gfx::Size calcFrameSize(gfx::Size availSize,
|
||||
int cols, int rows,
|
||||
const gfx::Point& frameOrigin,
|
||||
const gfx::Size& padding)
|
||||
{
|
||||
if (cols <= 0)
|
||||
cols = 1;
|
||||
if (rows <= 0)
|
||||
rows = 1;
|
||||
|
||||
availSize.w -= (frameOrigin.x + padding.w*(cols-1));
|
||||
availSize.h -= (frameOrigin.y + padding.h*(rows-1));
|
||||
|
||||
return gfx::Size(availSize.w / cols, availSize.h / rows);
|
||||
}
|
||||
|
||||
struct ImportSpriteSheetParams : public NewParams {
|
||||
Param<bool> ui { this, true, "ui" };
|
||||
Param<app::SpriteSheetType> type { this, app::SpriteSheetType::None, "type" };
|
||||
Param<gfx::Rect> frameBounds { this, gfx::Rect(0, 0, 0, 0), "frameBounds" };
|
||||
Param<gfx::Size> padding { this, gfx::Size(0, 0), "padding" };
|
||||
Param<bool> partialTiles { this, false, "partialTiles" };
|
||||
// Columns and rows are optional, and they just help in calculating
|
||||
// frameBounds automatically when the number of columns and rows are known
|
||||
// beforehand. So, if these are specified along frameBounds, then frameBounds
|
||||
// width/height might be recalculated.
|
||||
Param<int> columns { this, 0, "columns" };
|
||||
Param<int> rows { this, 0, "rows" };
|
||||
};
|
||||
|
||||
class ImportSpriteSheetWindow : public app::gen::ImportSpriteSheet
|
||||
, public SelectBoxDelegate {
|
||||
public:
|
||||
ImportSpriteSheetWindow(Context* context)
|
||||
ImportSpriteSheetWindow(const ImportSpriteSheetParams& params,
|
||||
Context* context)
|
||||
: m_context(context)
|
||||
, m_document(NULL)
|
||||
, m_editor(NULL)
|
||||
@ -81,6 +104,8 @@ public:
|
||||
y()->Change.connect([this]{ onEntriesChange(); });
|
||||
width()->Change.connect([this]{ onEntriesChange(); });
|
||||
height()->Change.connect([this]{ onEntriesChange(); });
|
||||
columns()->Change.connect([this]{ onColumnsChange(); });
|
||||
rows()->Change.connect([this]{ onRowsChange(); });
|
||||
paddingEnabled()->Click.connect([this]{ onPaddingEnabledChange(); });
|
||||
horizontalPadding()->Change.connect([this]{ onEntriesChange(); });
|
||||
verticalPadding()->Change.connect([this]{ onEntriesChange(); });
|
||||
@ -96,7 +121,40 @@ public:
|
||||
m_fileOpened = false;
|
||||
}
|
||||
|
||||
if (params.type.isSet())
|
||||
sheetType()->setSelectedItemIndex((int)params.type()-1);
|
||||
|
||||
if (params.frameBounds.isSet()) {
|
||||
x()->setTextf("%d", params.frameBounds().x);
|
||||
y()->setTextf("%d", params.frameBounds().y);
|
||||
width()->setTextf("%d", params.frameBounds().w);
|
||||
height()->setTextf("%d", params.frameBounds().h);
|
||||
}
|
||||
|
||||
paddingEnabled()->setSelected(params.padding.isSet());
|
||||
if (params.padding.isSet()) {
|
||||
if (m_docPref)
|
||||
m_docPref->importSpriteSheet.paddingBounds(params.padding());
|
||||
else {
|
||||
horizontalPadding()->setTextf("%d", params.padding().w);
|
||||
verticalPadding()->setTextf("%d", params.padding().h);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.partialTiles.isSet())
|
||||
partialTiles()->setSelected(params.partialTiles());
|
||||
|
||||
onPaddingEnabledChange();
|
||||
|
||||
if (params.columns.isSet()) {
|
||||
columns()->setTextf("%d", params.columns());
|
||||
onColumnsChange();
|
||||
}
|
||||
|
||||
if (params.rows.isSet()) {
|
||||
rows()->setTextf("%d", params.rows());
|
||||
onRowsChange();
|
||||
}
|
||||
}
|
||||
|
||||
~ImportSpriteSheetWindow() {
|
||||
@ -150,6 +208,7 @@ protected:
|
||||
|
||||
void onSheetTypeChange() {
|
||||
updateGridState();
|
||||
onEntriesChange();
|
||||
}
|
||||
|
||||
void onSelectFile() {
|
||||
@ -187,7 +246,7 @@ protected:
|
||||
return gfx::Size(padW, padH);
|
||||
}
|
||||
|
||||
void onEntriesChange() {
|
||||
void updateRulers() {
|
||||
m_rect = getRectFromEntries();
|
||||
m_padding = getPaddingFromEntries();
|
||||
|
||||
@ -205,6 +264,32 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
void onEntriesChange() {
|
||||
updateRulers();
|
||||
columns()->setTextf("%d", calcColumns());
|
||||
rows()->setTextf("%d", calcRows());
|
||||
}
|
||||
|
||||
#define ON_COLROWS_CHANGE(widtheigth, wh) \
|
||||
if (!m_editor) \
|
||||
return; \
|
||||
auto frameSize = calcFrameSize(m_editor->sprite()->size(), \
|
||||
columns()->textInt(), rows()->textInt(), \
|
||||
getRectFromEntries().origin(), \
|
||||
getPaddingFromEntries()); \
|
||||
widtheigth()->setTextf("%d", frameSize.wh); \
|
||||
updateRulers();
|
||||
|
||||
void onColumnsChange() {
|
||||
ON_COLROWS_CHANGE(width, w);
|
||||
}
|
||||
|
||||
void onRowsChange() {
|
||||
ON_COLROWS_CHANGE(height, h);
|
||||
}
|
||||
|
||||
#undef ON_COLROWS_CHANGE
|
||||
|
||||
bool onProcessMessage(ui::Message* msg) override {
|
||||
switch (msg->type()) {
|
||||
case kCloseMessage:
|
||||
@ -223,6 +308,35 @@ protected:
|
||||
targets.push_back(View::getView(m_editor));
|
||||
}
|
||||
|
||||
#define CALC_SPANS(xy, wh) \
|
||||
if (!m_editor) \
|
||||
return 0; \
|
||||
int spans = 0; \
|
||||
int rectwh = (m_rect.wh <= 0 ? 1 : m_rect.wh); \
|
||||
int wh = m_editor->sprite()->size().wh - m_rect.xy; \
|
||||
while(wh > 0) { \
|
||||
wh -= rectwh; \
|
||||
if (wh >= 0) { \
|
||||
spans++; \
|
||||
} \
|
||||
wh -= m_padding.wh; \
|
||||
} \
|
||||
return spans;
|
||||
|
||||
int calcColumns() {
|
||||
if (sheetTypeValue() == SpriteSheetType::Vertical)
|
||||
return 1;
|
||||
CALC_SPANS(x, w);
|
||||
}
|
||||
|
||||
int calcRows() {
|
||||
if (sheetTypeValue() == SpriteSheetType::Horizontal)
|
||||
return 1;
|
||||
CALC_SPANS(y, h);
|
||||
}
|
||||
|
||||
#undef CALC_SPANS
|
||||
|
||||
// SelectBoxDelegate impleentation
|
||||
void onChangeRectangle(const gfx::Rect& rect) override {
|
||||
m_rect = rect;
|
||||
@ -231,6 +345,8 @@ protected:
|
||||
y()->setTextf("%d", m_rect.y);
|
||||
width()->setTextf("%d", m_rect.w);
|
||||
height()->setTextf("%d", m_rect.h);
|
||||
columns()->setTextf("%d", calcColumns());
|
||||
rows()->setTextf("%d", calcRows());
|
||||
}
|
||||
|
||||
void onChangePadding(const gfx::Size& padding) override {
|
||||
@ -248,6 +364,8 @@ protected:
|
||||
horizontalPadding()->setTextf("%d", 0);
|
||||
verticalPadding()->setTextf("%d", 0);
|
||||
}
|
||||
columns()->setTextf("%d", calcColumns());
|
||||
rows()->setTextf("%d", calcRows());
|
||||
}
|
||||
|
||||
std::string onGetContextBarHelp() override {
|
||||
@ -340,13 +458,19 @@ private:
|
||||
switch (sheetTypeValue()) {
|
||||
case SpriteSheetType::Horizontal:
|
||||
flags |= int(SelectBoxState::Flags::HGrid);
|
||||
columns()->setEnabled(true);
|
||||
rows()->setEnabled(false);
|
||||
break;
|
||||
case SpriteSheetType::Vertical:
|
||||
flags |= int(SelectBoxState::Flags::VGrid);
|
||||
columns()->setEnabled(false);
|
||||
rows()->setEnabled(true);
|
||||
break;
|
||||
case SpriteSheetType::Rows:
|
||||
case SpriteSheetType::Columns:
|
||||
flags |= int(SelectBoxState::Flags::Grid);
|
||||
columns()->setEnabled(true);
|
||||
rows()->setEnabled(true);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -417,9 +541,7 @@ void ImportSpriteSheetCommand::onExecute(Context* context)
|
||||
auto& params = this->params();
|
||||
|
||||
if (context->isUIAvailable() && params.ui()) {
|
||||
// TODO use params as input values for the ImportSpriteSheetWindow
|
||||
|
||||
ImportSpriteSheetWindow window(context);
|
||||
ImportSpriteSheetWindow window(params, context);
|
||||
window.openWindowInForeground();
|
||||
if (!window.ok())
|
||||
return;
|
||||
@ -443,6 +565,24 @@ void ImportSpriteSheetCommand::onExecute(Context* context)
|
||||
document = context->activeDocument();
|
||||
if (!document)
|
||||
return;
|
||||
|
||||
Sprite* sprite = document->sprite();
|
||||
|
||||
auto newFrameBounds = calcFrameSize(sprite->size(),
|
||||
params.columns(), params.rows(),
|
||||
params.frameBounds().origin(),
|
||||
params.padding());
|
||||
if (params.columns.isSet()) {
|
||||
auto fb = params.frameBounds();
|
||||
fb.w = newFrameBounds.w;
|
||||
params.frameBounds(fb);
|
||||
}
|
||||
|
||||
if (params.rows.isSet()) {
|
||||
auto fb = params.frameBounds();
|
||||
fb.h = newFrameBounds.h;
|
||||
params.frameBounds(fb);
|
||||
}
|
||||
}
|
||||
|
||||
// The list of frames imported from the sheet
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include "app/color.h"
|
||||
#include "app/color_utils.h"
|
||||
#include "app/commands/command.h"
|
||||
#include "app/commands/new_params.h"
|
||||
#include "app/console.h"
|
||||
#include "app/context.h"
|
||||
#include "app/context_access.h"
|
||||
@ -48,22 +49,96 @@ using namespace ui;
|
||||
|
||||
static const char* ConfigSection = "MaskColor";
|
||||
|
||||
class MaskByColorCommand : public Command {
|
||||
public:
|
||||
MaskByColorCommand();
|
||||
struct MaskByColorParams : public NewParams {
|
||||
Param<bool> ui { this, true, "ui" };
|
||||
Param<app::Color> color { this, app::Color(), "color" };
|
||||
Param<int> tolerance { this, 0, "tolerance" };
|
||||
Param<gen::SelectionMode> mode { this, gen::SelectionMode::DEFAULT, "mode" };
|
||||
};
|
||||
|
||||
protected:
|
||||
bool onEnabled(Context* context) override;
|
||||
void onExecute(Context* context) override;
|
||||
class MaskByColorWindow : public ui::Window {
|
||||
public:
|
||||
MaskByColorWindow(MaskByColorParams& params, const ContextReader& reader)
|
||||
: Window(Window::WithTitleBar, Strings::mask_by_color_title())
|
||||
, m_reader(&reader)
|
||||
// Save original mask visibility to process it correctly in
|
||||
// ADD/SUBTRACT/INTERSECT Selection Mode
|
||||
, m_isOrigMaskVisible(reader.document()->isMaskVisible()) {
|
||||
|
||||
TooltipManager* tooltipManager = new TooltipManager();
|
||||
addChild(tooltipManager);
|
||||
auto box1 = new Box(VERTICAL);
|
||||
auto box2 = new Box(HORIZONTAL);
|
||||
auto box3 = new Box(HORIZONTAL);
|
||||
auto box4 = new Box(HORIZONTAL | HOMOGENEOUS);
|
||||
auto label_color = new Label(Strings::mask_by_color_label_color());
|
||||
m_buttonColor = new ColorButton(
|
||||
params.color(),
|
||||
reader.sprite()->pixelFormat(),
|
||||
ColorButtonOptions());
|
||||
auto label_tolerance = new Label(Strings::mask_by_color_tolerance());
|
||||
m_sliderTolerance = new Slider(0, 255, params.tolerance());
|
||||
|
||||
m_selMode = new SelModeField;
|
||||
m_selMode->setSelectionMode(params.mode());
|
||||
m_selMode->setupTooltips(tooltipManager);
|
||||
|
||||
m_checkPreview = new CheckBox(Strings::mask_by_color_preview());
|
||||
m_buttonOk = new Button(Strings::mask_by_color_ok());
|
||||
auto button_cancel = new Button(Strings::mask_by_color_cancel());
|
||||
|
||||
m_checkPreview->processMnemonicFromText();
|
||||
m_buttonOk->processMnemonicFromText();
|
||||
button_cancel->processMnemonicFromText();
|
||||
|
||||
if (get_config_bool(ConfigSection, "Preview", true))
|
||||
m_checkPreview->setSelected(true);
|
||||
|
||||
m_buttonOk->Click.connect([this]{ closeWindow(m_buttonOk); });
|
||||
button_cancel->Click.connect([this, button_cancel]{ closeWindow(button_cancel); });
|
||||
|
||||
m_buttonColor->Change.connect([&]{ maskPreview(); });
|
||||
m_sliderTolerance->Change.connect([&]{ maskPreview(); });
|
||||
m_checkPreview->Click.connect([&]{ maskPreview(); });
|
||||
m_selMode->ModeChange.connect([&]{ maskPreview(); });
|
||||
|
||||
m_buttonOk->setFocusMagnet(true);
|
||||
m_buttonColor->setExpansive(true);
|
||||
m_sliderTolerance->setExpansive(true);
|
||||
box2->setExpansive(true);
|
||||
|
||||
addChild(box1);
|
||||
box1->addChild(m_selMode);
|
||||
box1->addChild(box2);
|
||||
box1->addChild(box3);
|
||||
box1->addChild(m_checkPreview);
|
||||
box1->addChild(box4);
|
||||
box2->addChild(label_color);
|
||||
box2->addChild(m_buttonColor);
|
||||
box3->addChild(label_tolerance);
|
||||
box3->addChild(m_sliderTolerance);
|
||||
box4->addChild(m_buttonOk);
|
||||
box4->addChild(button_cancel);
|
||||
|
||||
// Default position
|
||||
remapWindow();
|
||||
centerWindow();
|
||||
|
||||
// Mask first preview
|
||||
maskPreview();
|
||||
}
|
||||
|
||||
bool accepted() const { return closer() == m_buttonOk; }
|
||||
|
||||
app::Color getColor() const { return m_buttonColor->getColor(); }
|
||||
|
||||
int getTolerance() const { return m_sliderTolerance->getValue(); }
|
||||
|
||||
gen::SelectionMode getSelectionMode() const { return m_selMode->selectionMode(); }
|
||||
|
||||
bool isPreviewChecked() const { return m_checkPreview->isSelected(); }
|
||||
|
||||
private:
|
||||
Mask* generateMask(const Mask& origMask,
|
||||
const Sprite* sprite,
|
||||
const Image* image,
|
||||
int xpos, int ypos,
|
||||
gen::SelectionMode mode);
|
||||
void maskPreview(const ContextReader& reader);
|
||||
|
||||
class SelModeField : public SelectionModeField {
|
||||
public:
|
||||
obs::signal<void()> ModeChange;
|
||||
@ -73,157 +148,31 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
Window* m_window = nullptr;
|
||||
void maskPreview();
|
||||
|
||||
const ContextReader* m_reader = nullptr;
|
||||
bool m_isOrigMaskVisible;
|
||||
Button* m_buttonOk = nullptr;
|
||||
ColorButton* m_buttonColor = nullptr;
|
||||
CheckBox* m_checkPreview = nullptr;
|
||||
Slider* m_sliderTolerance = nullptr;
|
||||
SelModeField* m_selMode = nullptr;
|
||||
bool m_isOrigMaskVisible;
|
||||
|
||||
};
|
||||
|
||||
MaskByColorCommand::MaskByColorCommand()
|
||||
: Command(CommandId::MaskByColor(), CmdUIOnlyFlag)
|
||||
static Mask* generateMask(const Mask& origMask,
|
||||
bool isOrigMaskVisible,
|
||||
const Image* image,
|
||||
int xpos, int ypos,
|
||||
gen::SelectionMode mode,
|
||||
int color,
|
||||
int tolerance)
|
||||
{
|
||||
}
|
||||
|
||||
bool MaskByColorCommand::onEnabled(Context* context)
|
||||
{
|
||||
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
|
||||
ContextFlags::HasActiveSprite |
|
||||
ContextFlags::HasActiveImage);
|
||||
}
|
||||
|
||||
void MaskByColorCommand::onExecute(Context* context)
|
||||
{
|
||||
ASSERT(!m_window);
|
||||
|
||||
const ContextReader reader(context);
|
||||
const Sprite* sprite = reader.sprite();
|
||||
|
||||
if (!App::instance()->isGui() || !sprite)
|
||||
return;
|
||||
|
||||
int xpos, ypos;
|
||||
const Image* image = reader.image(&xpos, &ypos);
|
||||
if (!image)
|
||||
return;
|
||||
|
||||
std::unique_ptr<Window> win(
|
||||
new Window(Window::WithTitleBar, Strings::mask_by_color_title()));
|
||||
base::ScopedValue<Window*> setWindow(m_window, win.get(), nullptr);
|
||||
TooltipManager* tooltipManager = new TooltipManager();
|
||||
m_window->addChild(tooltipManager);
|
||||
auto box1 = new Box(VERTICAL);
|
||||
auto box2 = new Box(HORIZONTAL);
|
||||
auto box3 = new Box(HORIZONTAL);
|
||||
auto box4 = new Box(HORIZONTAL | HOMOGENEOUS);
|
||||
auto label_color = new Label(Strings::mask_by_color_label_color());
|
||||
m_buttonColor = new ColorButton(
|
||||
ColorBar::instance()->getFgColor(),
|
||||
sprite->pixelFormat(),
|
||||
ColorButtonOptions());
|
||||
auto label_tolerance = new Label(Strings::mask_by_color_tolerance());
|
||||
m_sliderTolerance = new Slider(0, 255, get_config_int(ConfigSection, "Tolerance", 0));
|
||||
|
||||
m_selMode = new SelModeField;
|
||||
m_selMode->setupTooltips(tooltipManager);
|
||||
|
||||
m_checkPreview = new CheckBox(Strings::mask_by_color_preview());
|
||||
auto button_ok = new Button(Strings::mask_by_color_ok());
|
||||
auto button_cancel = new Button(Strings::mask_by_color_cancel());
|
||||
|
||||
m_checkPreview->processMnemonicFromText();
|
||||
button_ok->processMnemonicFromText();
|
||||
button_cancel->processMnemonicFromText();
|
||||
|
||||
if (get_config_bool(ConfigSection, "Preview", true))
|
||||
m_checkPreview->setSelected(true);
|
||||
|
||||
button_ok->Click.connect([this, button_ok]{ m_window->closeWindow(button_ok); });
|
||||
button_cancel->Click.connect([this, button_cancel]{ m_window->closeWindow(button_cancel); });
|
||||
|
||||
m_buttonColor->Change.connect([&]{ maskPreview(reader); });
|
||||
m_sliderTolerance->Change.connect([&]{ maskPreview(reader); });
|
||||
m_checkPreview->Click.connect([&]{ maskPreview(reader); });
|
||||
m_selMode->ModeChange.connect([&]{ maskPreview(reader); });
|
||||
|
||||
button_ok->setFocusMagnet(true);
|
||||
m_buttonColor->setExpansive(true);
|
||||
m_sliderTolerance->setExpansive(true);
|
||||
box2->setExpansive(true);
|
||||
|
||||
m_window->addChild(box1);
|
||||
box1->addChild(m_selMode);
|
||||
box1->addChild(box2);
|
||||
box1->addChild(box3);
|
||||
box1->addChild(m_checkPreview);
|
||||
box1->addChild(box4);
|
||||
box2->addChild(label_color);
|
||||
box2->addChild(m_buttonColor);
|
||||
box3->addChild(label_tolerance);
|
||||
box3->addChild(m_sliderTolerance);
|
||||
box4->addChild(button_ok);
|
||||
box4->addChild(button_cancel);
|
||||
|
||||
// Default position
|
||||
m_window->remapWindow();
|
||||
m_window->centerWindow();
|
||||
|
||||
// Save original mask visibility to process it correctly in
|
||||
// ADD/SUBTRACT/INTERSECT Selection Mode
|
||||
m_isOrigMaskVisible = reader.document()->isMaskVisible();
|
||||
|
||||
// Mask first preview
|
||||
maskPreview(reader);
|
||||
|
||||
// Load window configuration
|
||||
load_window_pos(m_window, ConfigSection);
|
||||
|
||||
// Open the window
|
||||
m_window->openWindowInForeground();
|
||||
|
||||
bool apply = (m_window->closer() == button_ok);
|
||||
|
||||
ContextWriter writer(reader);
|
||||
Doc* document(writer.document());
|
||||
|
||||
if (apply) {
|
||||
Tx tx(writer, "Mask by Color", DoesntModifyDocument);
|
||||
std::unique_ptr<Mask> mask(generateMask(*document->mask(),
|
||||
sprite, image, xpos, ypos,
|
||||
m_selMode->selectionMode()));
|
||||
tx(new cmd::SetMask(document, mask.get()));
|
||||
tx.commit();
|
||||
|
||||
set_config_int(ConfigSection, "Tolerance", m_sliderTolerance->getValue());
|
||||
set_config_bool(ConfigSection, "Preview", m_checkPreview->isSelected());
|
||||
}
|
||||
else {
|
||||
document->generateMaskBoundaries();
|
||||
}
|
||||
|
||||
// Update boundaries and editors.
|
||||
update_screen_for_document(document);
|
||||
|
||||
// Save window configuration.
|
||||
save_window_pos(m_window, ConfigSection);
|
||||
}
|
||||
|
||||
Mask* MaskByColorCommand::generateMask(const Mask& origMask,
|
||||
const Sprite* sprite,
|
||||
const Image* image,
|
||||
int xpos, int ypos,
|
||||
gen::SelectionMode mode)
|
||||
{
|
||||
int color = color_utils::color_for_image(m_buttonColor->getColor(),
|
||||
sprite->pixelFormat());
|
||||
int tolerance = m_sliderTolerance->getValue();
|
||||
|
||||
std::unique_ptr<Mask> mask(new Mask());
|
||||
mask->byColor(image, color, tolerance);
|
||||
mask->offsetOrigin(xpos, ypos);
|
||||
|
||||
if (!origMask.isEmpty() && m_isOrigMaskVisible) {
|
||||
if (!origMask.isEmpty() && isOrigMaskVisible) {
|
||||
switch (mode) {
|
||||
case gen::SelectionMode::DEFAULT:
|
||||
break;
|
||||
@ -251,18 +200,123 @@ Mask* MaskByColorCommand::generateMask(const Mask& origMask,
|
||||
return mask.release();
|
||||
}
|
||||
|
||||
void MaskByColorCommand::maskPreview(const ContextReader& reader)
|
||||
{
|
||||
ASSERT(m_window);
|
||||
if (m_window && m_checkPreview->isSelected()) {
|
||||
int xpos, ypos;
|
||||
const Image* image = reader.image(&xpos, &ypos);
|
||||
std::unique_ptr<Mask> mask(generateMask(*reader.document()->mask(),
|
||||
reader.sprite(), image,
|
||||
xpos, ypos,
|
||||
m_selMode->selectionMode()));
|
||||
class MaskByColorCommand : public CommandWithNewParams<MaskByColorParams> {
|
||||
public:
|
||||
MaskByColorCommand();
|
||||
|
||||
ContextWriter writer(reader);
|
||||
protected:
|
||||
bool onEnabled(Context* context) override;
|
||||
void onExecute(Context* context) override;
|
||||
|
||||
};
|
||||
|
||||
MaskByColorCommand::MaskByColorCommand()
|
||||
: CommandWithNewParams(CommandId::MaskByColor(), CmdUIOnlyFlag)
|
||||
{
|
||||
}
|
||||
|
||||
bool MaskByColorCommand::onEnabled(Context* context)
|
||||
{
|
||||
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
|
||||
ContextFlags::HasActiveSprite |
|
||||
ContextFlags::HasActiveImage);
|
||||
}
|
||||
|
||||
void MaskByColorCommand::onExecute(Context* context)
|
||||
{
|
||||
const bool ui = (params().ui() && context->isUIAvailable());
|
||||
const ContextReader reader(context);
|
||||
const Sprite* sprite = reader.sprite();
|
||||
|
||||
if (!sprite)
|
||||
return;
|
||||
|
||||
int xpos, ypos;
|
||||
const Image* image = reader.image(&xpos, &ypos);
|
||||
if (!image)
|
||||
return;
|
||||
|
||||
// Save original mask visibility to process it correctly in
|
||||
// ADD/SUBTRACT/INTERSECT Selection Mode
|
||||
bool isOrigMaskVisible = reader.document()->isMaskVisible();
|
||||
|
||||
bool apply = true;
|
||||
auto& params = this->params();
|
||||
|
||||
// If UI is available, set parameters default values from the UI/configuration
|
||||
if (context->isUIAvailable()) {
|
||||
if (!params.color.isSet())
|
||||
params.color(ColorBar::instance()->getFgColor());
|
||||
|
||||
if (!params.tolerance.isSet())
|
||||
params.tolerance(get_config_int(ConfigSection, "Tolerance", 0));
|
||||
|
||||
if (!params.mode.isSet())
|
||||
params.mode(Preferences::instance().selection.mode());
|
||||
}
|
||||
|
||||
if (ui) {
|
||||
MaskByColorWindow window(params, reader);
|
||||
|
||||
// Load window configuration
|
||||
load_window_pos(&window, ConfigSection);
|
||||
|
||||
// Open the window
|
||||
window.openWindowInForeground();
|
||||
|
||||
// Save window configuration.
|
||||
save_window_pos(&window, ConfigSection);
|
||||
|
||||
apply = window.accepted();
|
||||
if (apply) {
|
||||
params.color(window.getColor());
|
||||
params.mode(window.getSelectionMode());
|
||||
params.tolerance(window.getTolerance());
|
||||
|
||||
set_config_int(ConfigSection, "Tolerance", params.tolerance());
|
||||
set_config_bool(ConfigSection, "Preview", window.isPreviewChecked());
|
||||
}
|
||||
}
|
||||
|
||||
ContextWriter writer(reader);
|
||||
Doc* document(writer.document());
|
||||
|
||||
if (apply) {
|
||||
int color = color_utils::color_for_image(params.color(),
|
||||
sprite->pixelFormat());
|
||||
|
||||
Tx tx(writer, "Mask by Color", DoesntModifyDocument);
|
||||
std::unique_ptr<Mask> mask(generateMask(*document->mask(),
|
||||
isOrigMaskVisible,
|
||||
image, xpos, ypos,
|
||||
params.mode(),
|
||||
color, params.tolerance()));
|
||||
tx(new cmd::SetMask(document, mask.get()));
|
||||
tx.commit();
|
||||
}
|
||||
else {
|
||||
document->generateMaskBoundaries();
|
||||
}
|
||||
|
||||
// Update boundaries and editors.
|
||||
update_screen_for_document(document);
|
||||
}
|
||||
|
||||
void MaskByColorWindow::maskPreview()
|
||||
{
|
||||
if (isPreviewChecked()) {
|
||||
int xpos, ypos;
|
||||
const Image* image = m_reader->image(&xpos, &ypos);
|
||||
int color = color_utils::color_for_image(m_buttonColor->getColor(),
|
||||
m_reader->sprite()->pixelFormat());
|
||||
int tolerance = m_sliderTolerance->getValue();
|
||||
std::unique_ptr<Mask> mask(generateMask(*m_reader->document()->mask(),
|
||||
m_isOrigMaskVisible,
|
||||
image, xpos, ypos,
|
||||
m_selMode->selectionMode(),
|
||||
color, tolerance));
|
||||
|
||||
ContextWriter writer(*m_reader);
|
||||
|
||||
#ifdef SHOW_BOUNDARIES_GEN_PERFORMANCE
|
||||
base::Chrono chrono;
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
#include "app/color.h"
|
||||
#include "app/doc_exporter.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/sprite_sheet_type.h"
|
||||
#include "app/tools/ink_type.h"
|
||||
#include "base/convert_to.h"
|
||||
@ -241,6 +242,21 @@ void Param<doc::RgbMapAlgorithm>::fromString(const std::string& value)
|
||||
setValue(doc::RgbMapAlgorithm::DEFAULT);
|
||||
}
|
||||
|
||||
template<>
|
||||
void Param<gen::SelectionMode>::fromString(const std::string& value)
|
||||
{
|
||||
if (base::utf8_icmp(value, "replace") == 0)
|
||||
setValue(gen::SelectionMode::REPLACE);
|
||||
else if (base::utf8_icmp(value, "add") == 0)
|
||||
setValue(gen::SelectionMode::ADD);
|
||||
else if (base::utf8_icmp(value, "subtract") == 0)
|
||||
setValue(gen::SelectionMode::SUBTRACT);
|
||||
else if (base::utf8_icmp(value, "intersect") == 0)
|
||||
setValue(gen::SelectionMode::INTERSECT);
|
||||
else
|
||||
setValue(gen::SelectionMode::DEFAULT);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Convert values from Lua
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@ -405,6 +421,15 @@ void Param<doc::RgbMapAlgorithm>::fromLua(lua_State* L, int index)
|
||||
setValue((doc::RgbMapAlgorithm)lua_tointeger(L, index));
|
||||
}
|
||||
|
||||
template<>
|
||||
void Param<gen::SelectionMode>::fromLua(lua_State* L, int index)
|
||||
{
|
||||
if (lua_type(L, index) == LUA_TSTRING)
|
||||
fromString(lua_tostring(L, index));
|
||||
else
|
||||
setValue((gen::SelectionMode)lua_tointeger(L, index));
|
||||
}
|
||||
|
||||
void CommandWithNewParamsBase::loadParamsFromLuaTable(lua_State* L, int index)
|
||||
{
|
||||
onResetValues();
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2018-2023 Igara Studio S.A.
|
||||
// Copyright (C) 2018-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -9,6 +9,7 @@
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/cmd/remove_cel.h"
|
||||
#include "app/cmd/replace_image.h"
|
||||
#include "app/cmd/set_cel_opacity.h"
|
||||
#include "app/cmd/set_cel_position.h"
|
||||
@ -122,13 +123,16 @@ int Cel_set_frame(lua_State* L)
|
||||
int Cel_set_image(lua_State* L)
|
||||
{
|
||||
auto cel = get_docobj<Cel>(L, 1);
|
||||
auto srcImage = get_image_from_arg(L, 2);
|
||||
ImageRef newImage(Image::createCopy(srcImage));
|
||||
|
||||
Tx tx(cel->sprite());
|
||||
tx(new cmd::ReplaceImage(cel->sprite(),
|
||||
cel->imageRef(),
|
||||
newImage));
|
||||
if (may_get_obj<Image>(L, 2)) {
|
||||
const auto* srcImage = get_image_from_arg(L, 2);
|
||||
const ImageRef newImage(Image::createCopy(srcImage));
|
||||
tx(new cmd::ReplaceImage(cel->sprite(),
|
||||
cel->imageRef(),
|
||||
newImage));
|
||||
}
|
||||
else if (lua_isnil(L, 2))
|
||||
tx(new cmd::RemoveCel(cel));
|
||||
tx.commit();
|
||||
return 0;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -207,6 +207,7 @@ namespace app {
|
||||
virtual const doc::Grid& getGrid() const = 0;
|
||||
virtual gfx::Rect getGridBounds() = 0;
|
||||
virtual bool isPixelConnectivityEightConnected() = 0;
|
||||
virtual bool isPointInsideCanvas(const gfx::Point& point) = 0;
|
||||
|
||||
// Returns true if the figure must be filled when we release the
|
||||
// mouse (e.g. a filled rectangle, etc.)
|
||||
|
@ -153,9 +153,8 @@ bool ToolLoopManager::releaseButton(const Pointer& pointer)
|
||||
|
||||
if (m_toolLoop->getController()->isOnePoint() &&
|
||||
m_toolLoop->getInk()->isSelection() &&
|
||||
!m_toolLoop->getSrcImage()->bounds().contains(pointer.point())) {
|
||||
!m_toolLoop->isPointInsideCanvas(pointer.point()))
|
||||
return false;
|
||||
}
|
||||
|
||||
Stroke::Pt spritePoint = getSpriteStrokePt(pointer);
|
||||
bool res = m_toolLoop->getController()->releaseButton(m_stroke, spritePoint);
|
||||
|
@ -609,6 +609,12 @@ bool StandbyState::onUpdateStatusBar(Editor* editor)
|
||||
gfx::Point pt = grid.canvasToTile(gfx::Point(spritePos));
|
||||
buf += fmt::format(" :grid: {} {}", pt.x, pt.y);
|
||||
|
||||
// Number of columns of the current grid/tilemap to show a
|
||||
// "tile index", i.e. a linear index that might be used in a
|
||||
// 1D array to represent the current grid tile/cell. If it's 0
|
||||
// we don't show the index.
|
||||
int tileIndexColumns = 0;
|
||||
|
||||
// Show the tile index of this specific tile
|
||||
if (site.layer() &&
|
||||
site.layer()->isTilemap() &&
|
||||
@ -620,8 +626,20 @@ bool StandbyState::onUpdateStatusBar(Editor* editor)
|
||||
std::string str;
|
||||
build_tile_flags_string(tf, str);
|
||||
buf += fmt::format(" [{}{}]", ti, str);
|
||||
|
||||
// Show tile index for a tilemaps (using the tilemap size)
|
||||
tileIndexColumns = site.image()->width();
|
||||
}
|
||||
}
|
||||
// Show the tile index for a regular layer/grid
|
||||
else if (sprite->bounds().contains(gfx::Point(spritePos))) {
|
||||
tileIndexColumns =
|
||||
int(std::ceil(double(sprite->bounds().w - grid.origin().x)
|
||||
/ grid.tileSize().w));
|
||||
}
|
||||
|
||||
if (tileIndexColumns > 0 && pt.x >= 0 && pt.y >= 0)
|
||||
buf += fmt::format(" :search: {}", pt.x+pt.y*tileIndexColumns);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -360,6 +360,15 @@ public:
|
||||
== app::gen::PixelConnectivity::EIGHT_CONNECTED);
|
||||
}
|
||||
|
||||
bool isPointInsideCanvas(const gfx::Point& point) override {
|
||||
const int a = ((getTiledMode() == TiledMode::X_AXIS ||
|
||||
getTiledMode() == TiledMode::BOTH) ? 3 : 1);
|
||||
const int b = ((getTiledMode() == TiledMode::Y_AXIS ||
|
||||
getTiledMode() == TiledMode::BOTH) ? 3 : 1);
|
||||
return 0 <= point.x && point.x < a * sprite()->size().w &&
|
||||
0 <= point.y && point.y < b * sprite()->size().h;
|
||||
}
|
||||
|
||||
const doc::Grid& getGrid() const override { return m_grid; }
|
||||
gfx::Rect getGridBounds() override { return m_gridBounds; }
|
||||
gfx::Point getCelOrigin() override { return m_celOrigin; }
|
||||
|
@ -86,11 +86,7 @@ bool ButtonBase::onProcessMessage(Message* msg)
|
||||
|
||||
if (isEnabled() && isVisible()) {
|
||||
const bool mnemonicPressed =
|
||||
(mnemonic() &&
|
||||
(!mnemonicRequiresModifiers() ||
|
||||
msg->altPressed() ||
|
||||
msg->cmdPressed()) &&
|
||||
isMnemonicPressed(keymsg));
|
||||
isMnemonicPressedWithModifiers(keymsg);
|
||||
|
||||
// For kButtonWidget
|
||||
if (m_behaviorType == kButtonWidget) {
|
||||
@ -154,11 +150,19 @@ bool ButtonBase::onProcessMessage(Message* msg)
|
||||
break;
|
||||
|
||||
case kCheckWidget: {
|
||||
// Fire onClick() event
|
||||
onClick();
|
||||
return true;
|
||||
}
|
||||
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
|
||||
KeyScancode scancode = keymsg->scancode();
|
||||
const bool mnemonicPressed =
|
||||
isMnemonicPressedWithModifiers(keymsg);
|
||||
|
||||
// Fire the onClick() event only if the user pressed space or
|
||||
// Alt+the underscored letter of the checkbox label.
|
||||
if (scancode == kKeySpace || mnemonicPressed) {
|
||||
onClick();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -1586,6 +1586,15 @@ bool Widget::isMnemonicPressed(const KeyMessage* keyMsg) const
|
||||
(chr >= '0' && chr <= '9' && keyMsg->scancode() == (kKey0 + chr - '0'))));
|
||||
}
|
||||
|
||||
bool Widget::isMnemonicPressedWithModifiers(const KeyMessage* msg) const
|
||||
{
|
||||
return (mnemonic() &&
|
||||
(!mnemonicRequiresModifiers() ||
|
||||
msg->altPressed() ||
|
||||
msg->cmdPressed()) &&
|
||||
isMnemonicPressed(msg));
|
||||
}
|
||||
|
||||
bool Widget::onProcessMessage(Message* msg)
|
||||
{
|
||||
ASSERT(msg != nullptr);
|
||||
|
@ -388,7 +388,7 @@ namespace ui {
|
||||
|
||||
// Offer the capture to widgets of the given type. Returns true if
|
||||
// the capture was passed to other widget.
|
||||
bool offerCapture(ui::MouseMessage* mouseMsg, int widget_type);
|
||||
bool offerCapture(MouseMessage* mouseMsg, int widget_type);
|
||||
|
||||
// Returns lower-case letter that represet the mnemonic of the widget
|
||||
// (the underscored character, i.e. the letter after & symbol).
|
||||
@ -409,7 +409,11 @@ namespace ui {
|
||||
// Returns true if the mnemonic character is pressed (without modifiers).
|
||||
// TODO maybe we can add check for modifiers now that this
|
||||
// information is included in the Widget
|
||||
bool isMnemonicPressed(const ui::KeyMessage* keyMsg) const;
|
||||
bool isMnemonicPressed(const KeyMessage* keyMsg) const;
|
||||
|
||||
// Returns true if the mnemonic character is pressed with
|
||||
// modifiers (Alt or Command key).
|
||||
bool isMnemonicPressedWithModifiers(const KeyMessage* msg) const;
|
||||
|
||||
// Signals
|
||||
obs::signal<void()> InitTheme;
|
||||
|
@ -664,3 +664,102 @@ do
|
||||
expect_img(i, { 1, 1, 1, 1,
|
||||
1, 0, 0, 1 })
|
||||
end
|
||||
|
||||
-- MaskByColor
|
||||
do
|
||||
local s = Sprite(5, 5, ColorMode.INDEXED)
|
||||
local c = s.cels[1]
|
||||
local i = c.image
|
||||
array_to_pixels({
|
||||
1, 1, 0, 0, 1,
|
||||
1, 1, 0, 0, 1,
|
||||
1, 0, 0, 0, 1,
|
||||
1, 0, 0, 1, 1,
|
||||
1, 0, 0, 1, 1,
|
||||
}, i)
|
||||
|
||||
app.command.MaskByColor {
|
||||
color = Color{ index=1 },
|
||||
tolerance = 0,
|
||||
}
|
||||
app.fgColor = Color{ index=2 }
|
||||
app.command.Fill()
|
||||
|
||||
expect_img(i, {
|
||||
2, 2, 0, 0, 2,
|
||||
2, 2, 0, 0, 2,
|
||||
2, 0, 0, 0, 2,
|
||||
2, 0, 0, 2, 2,
|
||||
2, 0, 0, 2, 2,
|
||||
})
|
||||
|
||||
-- Subtract from current selection by color
|
||||
app.command.MaskAll {}
|
||||
app.command.MaskByColor {
|
||||
color = Color{ index=2 },
|
||||
tolerance = 0,
|
||||
mode = SelectionMode.SUBTRACT,
|
||||
}
|
||||
|
||||
app.fgColor = Color{ index=3 }
|
||||
app.command.Fill()
|
||||
|
||||
expect_img(i, {
|
||||
2, 2, 3, 3, 2,
|
||||
2, 2, 3, 3, 2,
|
||||
2, 3, 3, 3, 2,
|
||||
2, 3, 3, 2, 2,
|
||||
2, 3, 3, 2, 2,
|
||||
})
|
||||
|
||||
-- Add to current selection by color
|
||||
app.command.MaskByColor {
|
||||
color = Color{ index=2 },
|
||||
tolerance = 0,
|
||||
mode = SelectionMode.ADD,
|
||||
}
|
||||
app.fgColor = Color{ index=4 }
|
||||
app.command.Fill()
|
||||
|
||||
expect_img(i, {
|
||||
4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4,
|
||||
})
|
||||
|
||||
-- Reset image for new test
|
||||
array_to_pixels({
|
||||
1, 1, 0, 0, 1,
|
||||
1, 1, 0, 0, 1,
|
||||
1, 0, 0, 0, 1,
|
||||
1, 0, 0, 1, 1,
|
||||
1, 0, 0, 1, 1,
|
||||
}, i)
|
||||
|
||||
-- Select a centered 3x3 square
|
||||
app.command.MaskAll {}
|
||||
app.command.ModifySelection {
|
||||
modifier = 'contract',
|
||||
quantity = 1,
|
||||
brush = 'square'
|
||||
}
|
||||
-- Intersect with current selection by color
|
||||
app.command.MaskByColor {
|
||||
color = Color{ index=1 },
|
||||
tolerance = 0,
|
||||
mode = SelectionMode.INTERSECT,
|
||||
}
|
||||
|
||||
app.fgColor = Color{ index=2 }
|
||||
app.command.Fill()
|
||||
|
||||
expect_img(i, {
|
||||
1, 1, 0, 0, 1,
|
||||
1, 2, 0, 0, 1,
|
||||
1, 0, 0, 0, 1,
|
||||
1, 0, 0, 2, 1,
|
||||
1, 0, 0, 1, 1,
|
||||
})
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user