1
0
mirror of https://github.com/aseprite/aseprite.git synced 2025-04-02 13:20:12 +00:00
aseprite/src/app/ui/dynamics_popup.cpp
David Capello 8034b0cbcc Add support for multiple native windows (, , , etc.)
Each ui::Window now can have a related native os::Window. This
connection is done through the ui::Display class added recently in
c3d52f0bbe5242923c9e38495e1be1135ca0934f.
2021-03-02 13:50:49 -03:00

459 lines
14 KiB
C++

// Aseprite
// Copyright (C) 2020-2021 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/dynamics_popup.h"
#include "app/ui/dithering_selector.h"
#include "app/ui/skin/skin_theme.h"
#include "base/clamp.h"
#include "os/font.h"
#include "os/surface.h"
#include "ui/message.h"
#include "ui/paint_event.h"
#include "ui/scale.h"
#include "ui/size_hint_event.h"
#include "ui/widget.h"
#include "dynamics.xml.h"
#include <algorithm>
namespace app {
using namespace ui;
using namespace skin;
namespace {
enum {
NONE,
PRESSURE_HEADER,
VELOCITY_HEADER,
SIZE_HEADER,
SIZE_WITH_PRESSURE,
SIZE_WITH_VELOCITY,
ANGLE_HEADER,
ANGLE_WITH_PRESSURE,
ANGLE_WITH_VELOCITY,
GRADIENT_HEADER,
GRADIENT_WITH_PRESSURE,
GRADIENT_WITH_VELOCITY,
};
} // anonymous namespace
// Special slider to set the min/max threshold values of a sensor
class DynamicsPopup::ThresholdSlider : public Widget {
public:
ThresholdSlider() {
setExpansive(true);
initTheme();
}
float minThreshold() const { return m_minThreshold; }
float maxThreshold() const { return m_maxThreshold; }
void setSensorValue(float v) {
m_sensorValue = v;
invalidate();
}
private:
void onInitTheme(InitThemeEvent& ev) override {
Widget::onInitTheme(ev);
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
setBorder(
gfx::Border(
theme->parts.miniSliderEmpty()->bitmapW()->width(),
theme->parts.miniSliderEmpty()->bitmapN()->height(),
theme->parts.miniSliderEmpty()->bitmapE()->width(),
theme->parts.miniSliderEmpty()->bitmapS()->height()));
}
void onSizeHint(SizeHintEvent& ev) override {
ev.setSizeHint(
border().width(),
textHeight()+2*guiscale() + border().height());
}
void onPaint(PaintEvent& ev) override {
Graphics* g = ev.graphics();
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
gfx::Rect rc = clientBounds();
gfx::Color bgcolor = bgColor();
g->fillRect(bgcolor, rc);
rc.shrink(border());
rc = clientBounds();
// Draw customized background
const skin::SkinPartPtr& nw = theme->parts.miniSliderEmpty();
os::Surface* thumb =
(hasFocus() ? theme->parts.miniSliderThumbFocused()->bitmap(0):
theme->parts.miniSliderThumb()->bitmap(0));
// Draw background
g->fillRect(bgcolor, rc);
// Draw thumb
int thumb_y = rc.y;
rc.shrink(gfx::Border(0, thumb->height(), 0, 0));
// Draw borders
if (rc.h > 4*guiscale()) {
rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale());
theme->drawRect(g, rc, nw.get());
}
const int minX = this->minX();
const int maxX = this->maxX();
const int sensorW = float(rc.w)*m_sensorValue;
// Draw background
if (m_minThreshold > 0.0f) {
theme->drawRect(
g, gfx::Rect(rc.x, rc.y, minX-rc.x, rc.h),
theme->parts.miniSliderFull().get());
}
if (m_maxThreshold < 1.0f) {
theme->drawRect(
g, gfx::Rect(maxX, rc.y, rc.x2()-maxX, rc.h),
theme->parts.miniSliderFull().get());
}
g->fillRect(theme->colors.sliderEmptyText(),
gfx::Rect(rc.x, rc.y+rc.h/2-rc.h/8, sensorW, rc.h/4));
g->drawRgbaSurface(thumb, minX-thumb->width()/2, thumb_y);
g->drawRgbaSurface(thumb, maxX-thumb->width()/2, thumb_y);
}
bool onProcessMessage(Message* msg) override {
switch (msg->type()) {
case kMouseDownMessage: {
auto mouseMsg = static_cast<MouseMessage*>(msg);
const int u = mouseMsg->position().x - (origin().x+border().left()+3*guiscale());
const int minX = this->minX();
const int maxX = this->maxX();
if (ABS(u-minX) <
ABS(u-maxX))
capture = Capture::Min;
else
capture = Capture::Max;
captureMouse();
break;
}
case kMouseUpMessage:
if (hasCapture())
releaseMouse();
break;
case kMouseMoveMessage: {
if (!hasCapture())
break;
auto mouseMsg = static_cast<MouseMessage*>(msg);
const gfx::Rect rc = bounds();
float u = (mouseMsg->position().x - rc.x) / float(rc.w);
u = base::clamp(u, 0.0f, 1.0f);
switch (capture) {
case Capture::Min:
m_minThreshold = u;
if (m_maxThreshold < u)
m_maxThreshold = u;
invalidate();
break;
case Capture::Max:
m_maxThreshold = u;
if (m_minThreshold > u)
m_minThreshold = u;
invalidate();
break;
}
break;
}
}
return Widget::onProcessMessage(msg);
}
int minX() const {
gfx::Rect rc = clientBounds();
rc.shrink(border());
rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale());
return rc.x + float(rc.w)*m_minThreshold;
}
int maxX() const {
gfx::Rect rc = clientBounds();
rc.shrink(border());
rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale());
return rc.x + float(rc.w)*m_maxThreshold;
}
enum Capture { Min, Max };
float m_minThreshold = 0.1f;
float m_sensorValue = 0.0f;
float m_maxThreshold = 0.9f;
Capture capture;
};
DynamicsPopup::DynamicsPopup(Delegate* delegate)
: PopupWindow("",
PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion,
PopupWindow::EnterBehavior::DoNothingOnEnter)
, m_delegate(delegate)
, m_dynamics(new gen::Dynamics)
, m_ditheringSel(new DitheringSelector(DitheringSelector::SelectMatrix))
, m_fromTo(tools::ColorFromTo::BgToFg)
{
m_dynamics->values()->ItemChange.connect(
[this](ButtonSet::Item* item){
onValuesChange(item);
});
m_dynamics->maxSize()->Change.connect(
[this]{
m_delegate->setMaxSize(m_dynamics->maxSize()->getValue());
});
m_dynamics->maxAngle()->Change.connect(
[this]{
m_delegate->setMaxAngle(m_dynamics->maxAngle()->getValue());
});
m_dynamics->gradientFromTo()->Click.connect(
[this]{
if (m_fromTo == tools::ColorFromTo::BgToFg)
m_fromTo = tools::ColorFromTo::FgToBg;
else
m_fromTo = tools::ColorFromTo::BgToFg;
updateFromToText();
});
m_dynamics->gradientPlaceholder()->addChild(m_ditheringSel);
m_dynamics->pressurePlaceholder()->addChild(m_pressureThreshold = new ThresholdSlider);
m_dynamics->velocityPlaceholder()->addChild(m_velocityThreshold = new ThresholdSlider);
addChild(m_dynamics);
onValuesChange(nullptr);
}
tools::DynamicsOptions DynamicsPopup::getDynamics() const
{
tools::DynamicsOptions opts;
opts.size =
(isCheck(SIZE_WITH_PRESSURE) ? tools::DynamicSensor::Pressure:
isCheck(SIZE_WITH_VELOCITY) ? tools::DynamicSensor::Velocity:
tools::DynamicSensor::Static);
opts.angle =
(isCheck(ANGLE_WITH_PRESSURE) ? tools::DynamicSensor::Pressure:
isCheck(ANGLE_WITH_VELOCITY) ? tools::DynamicSensor::Velocity:
tools::DynamicSensor::Static);
opts.gradient =
(isCheck(GRADIENT_WITH_PRESSURE) ? tools::DynamicSensor::Pressure:
isCheck(GRADIENT_WITH_VELOCITY) ? tools::DynamicSensor::Velocity:
tools::DynamicSensor::Static);
opts.minSize = m_dynamics->minSize()->getValue();
opts.minAngle = m_dynamics->minAngle()->getValue();
opts.ditheringMatrix = m_ditheringSel->ditheringMatrix();
opts.colorFromTo = m_fromTo;
opts.minPressureThreshold = m_pressureThreshold->minThreshold();
opts.maxPressureThreshold = m_pressureThreshold->maxThreshold();
opts.minVelocityThreshold = m_velocityThreshold->minThreshold();
opts.maxVelocityThreshold = m_velocityThreshold->maxThreshold();
return opts;
}
void DynamicsPopup::setCheck(int i, bool state)
{
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
m_dynamics->values()
->getItem(i)
->setIcon(state ? theme->parts.dropPixelsOk(): nullptr);
}
bool DynamicsPopup::isCheck(int i) const
{
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
return (m_dynamics->values()
->getItem(i)
->icon() == theme->parts.dropPixelsOk());
}
void DynamicsPopup::onValuesChange(ButtonSet::Item* item)
{
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
const skin::SkinPartPtr& ok = theme->parts.dropPixelsOk();
const int i = (item ? m_dynamics->values()->getItemIndex(item): -1);
// Switch item off
if (item && item->icon().get() == ok.get()) {
item->setIcon(nullptr);
}
else {
switch (i) {
case SIZE_WITH_PRESSURE:
case SIZE_WITH_VELOCITY:
setCheck(SIZE_WITH_PRESSURE, i == SIZE_WITH_PRESSURE);
setCheck(SIZE_WITH_VELOCITY, i == SIZE_WITH_VELOCITY);
break;
case ANGLE_WITH_PRESSURE:
case ANGLE_WITH_VELOCITY:
setCheck(ANGLE_WITH_PRESSURE, i == ANGLE_WITH_PRESSURE);
setCheck(ANGLE_WITH_VELOCITY, i == ANGLE_WITH_VELOCITY);
break;
case GRADIENT_WITH_PRESSURE:
case GRADIENT_WITH_VELOCITY:
setCheck(GRADIENT_WITH_PRESSURE, i == GRADIENT_WITH_PRESSURE);
setCheck(GRADIENT_WITH_VELOCITY, i == GRADIENT_WITH_VELOCITY);
break;
}
}
const bool hasPressure = (isCheck(SIZE_WITH_PRESSURE) ||
isCheck(ANGLE_WITH_PRESSURE) ||
isCheck(GRADIENT_WITH_PRESSURE));
const bool hasVelocity = (isCheck(SIZE_WITH_VELOCITY) ||
isCheck(ANGLE_WITH_VELOCITY) ||
isCheck(GRADIENT_WITH_VELOCITY));
const bool needsSize = (isCheck(SIZE_WITH_PRESSURE) ||
isCheck(SIZE_WITH_VELOCITY));
const bool needsAngle = (isCheck(ANGLE_WITH_PRESSURE) ||
isCheck(ANGLE_WITH_VELOCITY));
const bool needsGradient = (isCheck(GRADIENT_WITH_PRESSURE) ||
isCheck(GRADIENT_WITH_VELOCITY));
const bool any = (needsSize || needsAngle || needsGradient);
doc::BrushRef brush = m_delegate->getActiveBrush();
if (needsSize && !m_dynamics->minSize()->isVisible()) {
m_dynamics->minSize()->setValue(1);
int maxSize = brush->size();
if (maxSize == 1) {
// If brush size == 1, we put it to 4 so the user has some size
// change by default.
maxSize = 4;
m_delegate->setMaxSize(maxSize);
}
m_dynamics->maxSize()->setValue(maxSize);
}
m_dynamics->sizeLabel()->setVisible(needsSize);
m_dynamics->minSize()->setVisible(needsSize);
m_dynamics->maxSize()->setVisible(needsSize);
if (needsAngle && !m_dynamics->minAngle()->isVisible()) {
m_dynamics->minAngle()->setValue(brush->angle());
m_dynamics->maxAngle()->setValue(brush->angle());
}
m_dynamics->angleLabel()->setVisible(needsAngle);
m_dynamics->minAngle()->setVisible(needsAngle);
m_dynamics->maxAngle()->setVisible(needsAngle);
m_dynamics->gradientLabel()->setVisible(needsGradient);
m_dynamics->gradientPlaceholder()->setVisible(needsGradient);
m_dynamics->gradientFromTo()->setVisible(needsGradient);
updateFromToText();
m_dynamics->separator()->setVisible(any);
m_dynamics->options()->setVisible(any);
m_dynamics->separator2()->setVisible(any);
m_dynamics->pressureLabel()->setVisible(hasPressure);
m_dynamics->pressurePlaceholder()->setVisible(hasPressure);
m_dynamics->velocityLabel()->setVisible(hasVelocity);
m_dynamics->velocityPlaceholder()->setVisible(hasVelocity);
auto oldBounds = bounds();
layout();
setBounds(gfx::Rect(origin(), sizeHint()));
m_hotRegion |= gfx::Region(bounds());
setHotRegion(m_hotRegion);
if (isVisible())
manager()->invalidateRect(oldBounds);
}
void DynamicsPopup::updateFromToText()
{
m_dynamics->gradientFromTo()->setText(
m_fromTo == tools::ColorFromTo::BgToFg ? "BG > FG":
m_fromTo == tools::ColorFromTo::FgToBg ? "FG > BG": "-");
}
void DynamicsPopup::updateWidgetsWithBrush()
{
doc::BrushRef brush = m_delegate->getActiveBrush();
m_dynamics->maxSize()->setValue(brush->size());
m_dynamics->maxAngle()->setValue(brush->angle());
}
bool DynamicsPopup::onProcessMessage(Message* msg)
{
switch (msg->type()) {
case kOpenMessage:
m_hotRegion = gfx::Region(bounds());
setHotRegion(m_hotRegion);
manager()->addMessageFilter(kMouseMoveMessage, this);
manager()->addMessageFilter(kMouseDownMessage, this);
disableFlags(IGNORE_MOUSE);
updateWidgetsWithBrush();
break;
case kCloseMessage:
m_hotRegion.clear();
manager()->removeMessageFilter(kMouseMoveMessage, this);
manager()->removeMessageFilter(kMouseDownMessage, this);
break;
case kMouseEnterMessage:
m_velocity.reset();
break;
case kMouseMoveMessage: {
auto mouseMsg = static_cast<MouseMessage*>(msg);
if (mouseMsg->pointerType() == PointerType::Pen ||
mouseMsg->pointerType() == PointerType::Eraser) {
if (m_dynamics->pressurePlaceholder()->isVisible()) {
m_pressureThreshold->setSensorValue(mouseMsg->pressure());
}
}
if (m_dynamics->velocityPlaceholder()->isVisible()) {
m_velocity.updateWithDisplayPoint(mouseMsg->position());
float v = m_velocity.velocity().magnitude()
/ tools::VelocitySensor::kScreenPixelsForFullVelocity;
v = base::clamp(v, 0.0f, 1.0f);
m_velocityThreshold->setSensorValue(v);
}
break;
}
case kMouseDownMessage: {
auto mouseMsg = static_cast<MouseMessage*>(msg);
auto picked = manager()->pick(mouseMsg->position());
if ((picked == nullptr) ||
(picked->window() != this &&
picked->window() != m_ditheringSel->getWindowWidget())) {
closeWindow(nullptr);
}
break;
}
}
return PopupWindow::onProcessMessage(msg);
}
} // namespace app