| 1 | // Aseprite |
| 2 | // Copyright (C) 2020-2022 Igara Studio S.A. |
| 3 | // |
| 4 | // This program is distributed under the terms of |
| 5 | // the End-User License Agreement for Aseprite. |
| 6 | |
| 7 | #ifdef HAVE_CONFIG_H |
| 8 | #include "config.h" |
| 9 | #endif |
| 10 | |
| 11 | #include "app/ui/dynamics_popup.h" |
| 12 | |
| 13 | #include "app/ui/dithering_selector.h" |
| 14 | #include "app/ui/skin/skin_theme.h" |
| 15 | #include "os/font.h" |
| 16 | #include "os/surface.h" |
| 17 | #include "ui/message.h" |
| 18 | #include "ui/paint_event.h" |
| 19 | #include "ui/scale.h" |
| 20 | #include "ui/size_hint_event.h" |
| 21 | #include "ui/widget.h" |
| 22 | |
| 23 | #include "dynamics.xml.h" |
| 24 | |
| 25 | #include <algorithm> |
| 26 | |
| 27 | namespace app { |
| 28 | |
| 29 | using namespace ui; |
| 30 | using namespace skin; |
| 31 | |
| 32 | namespace { |
| 33 | |
| 34 | enum { |
| 35 | NONE, |
| 36 | , |
| 37 | , |
| 38 | , |
| 39 | SIZE_WITH_PRESSURE, |
| 40 | SIZE_WITH_VELOCITY, |
| 41 | , |
| 42 | ANGLE_WITH_PRESSURE, |
| 43 | ANGLE_WITH_VELOCITY, |
| 44 | , |
| 45 | GRADIENT_WITH_PRESSURE, |
| 46 | GRADIENT_WITH_VELOCITY, |
| 47 | }; |
| 48 | |
| 49 | } // anonymous namespace |
| 50 | |
| 51 | // Special slider to set the min/max threshold values of a sensor |
| 52 | class DynamicsPopup:: : public Widget { |
| 53 | public: |
| 54 | () { |
| 55 | setExpansive(true); |
| 56 | initTheme(); |
| 57 | } |
| 58 | |
| 59 | float () const { return m_minThreshold; } |
| 60 | float () const { return m_maxThreshold; } |
| 61 | void (float v) { |
| 62 | m_sensorValue = v; |
| 63 | invalidate(); |
| 64 | } |
| 65 | |
| 66 | private: |
| 67 | void (InitThemeEvent& ev) override { |
| 68 | Widget::onInitTheme(ev); |
| 69 | auto theme = SkinTheme::get(this); |
| 70 | setBorder( |
| 71 | gfx::Border( |
| 72 | theme->parts.miniSliderEmpty()->bitmapW()->width(), |
| 73 | theme->parts.miniSliderEmpty()->bitmapN()->height(), |
| 74 | theme->parts.miniSliderEmpty()->bitmapE()->width(), |
| 75 | theme->parts.miniSliderEmpty()->bitmapS()->height())); |
| 76 | } |
| 77 | |
| 78 | void (SizeHintEvent& ev) override { |
| 79 | ev.setSizeHint( |
| 80 | border().width(), |
| 81 | textHeight()+2*guiscale() + border().height()); |
| 82 | } |
| 83 | |
| 84 | void (PaintEvent& ev) override { |
| 85 | Graphics* g = ev.graphics(); |
| 86 | auto theme = SkinTheme::get(this); |
| 87 | gfx::Rect rc = clientBounds(); |
| 88 | gfx::Color bgcolor = bgColor(); |
| 89 | g->fillRect(bgcolor, rc); |
| 90 | |
| 91 | rc.shrink(border()); |
| 92 | rc = clientBounds(); |
| 93 | |
| 94 | // Draw customized background |
| 95 | const skin::SkinPartPtr& nw = theme->parts.miniSliderEmpty(); |
| 96 | os::Surface* thumb = |
| 97 | (hasFocus() ? theme->parts.miniSliderThumbFocused()->bitmap(0): |
| 98 | theme->parts.miniSliderThumb()->bitmap(0)); |
| 99 | |
| 100 | // Draw background |
| 101 | g->fillRect(bgcolor, rc); |
| 102 | |
| 103 | // Draw thumb |
| 104 | int thumb_y = rc.y; |
| 105 | rc.shrink(gfx::Border(0, thumb->height(), 0, 0)); |
| 106 | |
| 107 | // Draw borders |
| 108 | if (rc.h > 4*guiscale()) { |
| 109 | rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale()); |
| 110 | theme->drawRect(g, rc, nw.get()); |
| 111 | } |
| 112 | |
| 113 | const int minX = this->minX(); |
| 114 | const int maxX = this->maxX(); |
| 115 | const int sensorW = float(rc.w)*m_sensorValue; |
| 116 | |
| 117 | // Draw background |
| 118 | if (m_minThreshold > 0.0f) { |
| 119 | theme->drawRect( |
| 120 | g, gfx::Rect(rc.x, rc.y, minX-rc.x, rc.h), |
| 121 | theme->parts.miniSliderFull().get()); |
| 122 | } |
| 123 | if (m_maxThreshold < 1.0f) { |
| 124 | theme->drawRect( |
| 125 | g, gfx::Rect(maxX, rc.y, rc.x2()-maxX, rc.h), |
| 126 | theme->parts.miniSliderFull().get()); |
| 127 | } |
| 128 | |
| 129 | g->fillRect(theme->colors.sliderEmptyText(), |
| 130 | gfx::Rect(rc.x, rc.y+rc.h/2-rc.h/8, sensorW, rc.h/4)); |
| 131 | |
| 132 | g->drawRgbaSurface(thumb, minX-thumb->width()/2, thumb_y); |
| 133 | g->drawRgbaSurface(thumb, maxX-thumb->width()/2, thumb_y); |
| 134 | } |
| 135 | |
| 136 | bool (Message* msg) override { |
| 137 | switch (msg->type()) { |
| 138 | |
| 139 | case kMouseDownMessage: { |
| 140 | auto mouseMsg = static_cast<MouseMessage*>(msg); |
| 141 | const int u = mouseMsg->position().x - (origin().x+border().left()+3*guiscale()); |
| 142 | const int minX = this->minX(); |
| 143 | const int maxX = this->maxX(); |
| 144 | if (ABS(u-minX) < |
| 145 | ABS(u-maxX)) |
| 146 | capture = Capture::Min; |
| 147 | else |
| 148 | capture = Capture::Max; |
| 149 | captureMouse(); |
| 150 | break; |
| 151 | } |
| 152 | |
| 153 | case kMouseUpMessage: |
| 154 | if (hasCapture()) |
| 155 | releaseMouse(); |
| 156 | break; |
| 157 | |
| 158 | case kMouseMoveMessage: { |
| 159 | if (!hasCapture()) |
| 160 | break; |
| 161 | |
| 162 | auto mouseMsg = static_cast<MouseMessage*>(msg); |
| 163 | const gfx::Rect rc = bounds(); |
| 164 | float u = (mouseMsg->position().x - rc.x) / float(rc.w); |
| 165 | u = std::clamp(u, 0.0f, 1.0f); |
| 166 | switch (capture) { |
| 167 | case Capture::Min: |
| 168 | m_minThreshold = u; |
| 169 | if (m_maxThreshold < u) |
| 170 | m_maxThreshold = u; |
| 171 | invalidate(); |
| 172 | break; |
| 173 | case Capture::Max: |
| 174 | m_maxThreshold = u; |
| 175 | if (m_minThreshold > u) |
| 176 | m_minThreshold = u; |
| 177 | invalidate(); |
| 178 | break; |
| 179 | } |
| 180 | break; |
| 181 | } |
| 182 | } |
| 183 | return Widget::onProcessMessage(msg); |
| 184 | } |
| 185 | |
| 186 | int () const { |
| 187 | gfx::Rect rc = clientBounds(); |
| 188 | rc.shrink(border()); |
| 189 | rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale()); |
| 190 | return rc.x + float(rc.w)*m_minThreshold; |
| 191 | } |
| 192 | |
| 193 | int () const { |
| 194 | gfx::Rect rc = clientBounds(); |
| 195 | rc.shrink(border()); |
| 196 | rc.shrink(gfx::Border(3, 0, 3, 1) * guiscale()); |
| 197 | return rc.x + float(rc.w)*m_maxThreshold; |
| 198 | } |
| 199 | |
| 200 | enum { , }; |
| 201 | |
| 202 | float = 0.1f; |
| 203 | float = 0.0f; |
| 204 | float = 0.9f; |
| 205 | Capture ; |
| 206 | }; |
| 207 | |
| 208 | DynamicsPopup::(Delegate* delegate) |
| 209 | : PopupWindow("" , |
| 210 | PopupWindow::ClickBehavior::CloseOnClickOutsideHotRegion, |
| 211 | PopupWindow::EnterBehavior::DoNothingOnEnter) |
| 212 | , m_delegate(delegate) |
| 213 | , m_dynamics(new gen::Dynamics) |
| 214 | , m_ditheringSel(new DitheringSelector(DitheringSelector::SelectMatrix)) |
| 215 | , m_fromTo(tools::ColorFromTo::BgToFg) |
| 216 | { |
| 217 | m_dynamics->stabilizer()->Click.connect( |
| 218 | [this](){ |
| 219 | if (m_dynamics->stabilizer()->isSelected() && |
| 220 | m_dynamics->stabilizerFactor()->getValue() == 0) { |
| 221 | // TODO default value when we enable stabilizer when it's zero |
| 222 | m_dynamics->stabilizerFactor()->setValue(16); |
| 223 | } |
| 224 | }); |
| 225 | m_dynamics->stabilizerFactor()->Change.connect( |
| 226 | [this](){ |
| 227 | m_dynamics->stabilizer()->setSelected(m_dynamics->stabilizerFactor()->getValue() > 0); |
| 228 | }); |
| 229 | |
| 230 | m_dynamics->values()->ItemChange.connect( |
| 231 | [this](ButtonSet::Item* item){ |
| 232 | onValuesChange(item); |
| 233 | }); |
| 234 | m_dynamics->maxSize()->Change.connect( |
| 235 | [this]{ |
| 236 | m_delegate->setMaxSize(m_dynamics->maxSize()->getValue()); |
| 237 | }); |
| 238 | m_dynamics->maxAngle()->Change.connect( |
| 239 | [this]{ |
| 240 | m_delegate->setMaxAngle(m_dynamics->maxAngle()->getValue()); |
| 241 | }); |
| 242 | m_dynamics->gradientFromTo()->Click.connect( |
| 243 | [this]{ |
| 244 | if (m_fromTo == tools::ColorFromTo::BgToFg) |
| 245 | m_fromTo = tools::ColorFromTo::FgToBg; |
| 246 | else |
| 247 | m_fromTo = tools::ColorFromTo::BgToFg; |
| 248 | updateFromToText(); |
| 249 | }); |
| 250 | m_ditheringSel->OpenListBox.connect( |
| 251 | [this]{ |
| 252 | if (auto comboboxWindow = m_ditheringSel->getWindowWidget()) { |
| 253 | m_hotRegion |= gfx::Region(comboboxWindow->boundsOnScreen()); |
| 254 | setHotRegion(m_hotRegion); |
| 255 | } |
| 256 | }); |
| 257 | |
| 258 | m_dynamics->gradientPlaceholder()->addChild(m_ditheringSel); |
| 259 | m_dynamics->pressurePlaceholder()->addChild(m_pressureThreshold = new ThresholdSlider); |
| 260 | m_dynamics->velocityPlaceholder()->addChild(m_velocityThreshold = new ThresholdSlider); |
| 261 | addChild(m_dynamics); |
| 262 | |
| 263 | onValuesChange(nullptr); |
| 264 | } |
| 265 | |
| 266 | void DynamicsPopup::(bool state) |
| 267 | { |
| 268 | m_dynamics->grid()->setVisible(state); |
| 269 | if (isVisible()) |
| 270 | expandWindow(sizeHint()); |
| 271 | } |
| 272 | |
| 273 | tools::DynamicsOptions DynamicsPopup::() const |
| 274 | { |
| 275 | tools::DynamicsOptions opts; |
| 276 | |
| 277 | if (m_dynamics->stabilizer()->isSelected()) { |
| 278 | opts.stabilizerFactor = m_dynamics->stabilizerFactor()->getValue(); |
| 279 | } |
| 280 | else { |
| 281 | opts.stabilizerFactor = 0; |
| 282 | } |
| 283 | |
| 284 | opts.size = |
| 285 | (isCheck(SIZE_WITH_PRESSURE) ? tools::DynamicSensor::Pressure: |
| 286 | isCheck(SIZE_WITH_VELOCITY) ? tools::DynamicSensor::Velocity: |
| 287 | tools::DynamicSensor::Static); |
| 288 | opts.angle = |
| 289 | (isCheck(ANGLE_WITH_PRESSURE) ? tools::DynamicSensor::Pressure: |
| 290 | isCheck(ANGLE_WITH_VELOCITY) ? tools::DynamicSensor::Velocity: |
| 291 | tools::DynamicSensor::Static); |
| 292 | opts.gradient = |
| 293 | (isCheck(GRADIENT_WITH_PRESSURE) ? tools::DynamicSensor::Pressure: |
| 294 | isCheck(GRADIENT_WITH_VELOCITY) ? tools::DynamicSensor::Velocity: |
| 295 | tools::DynamicSensor::Static); |
| 296 | opts.minSize = m_dynamics->minSize()->getValue(); |
| 297 | opts.minAngle = m_dynamics->minAngle()->getValue(); |
| 298 | opts.ditheringMatrix = m_ditheringSel->ditheringMatrix(); |
| 299 | opts.colorFromTo = m_fromTo; |
| 300 | |
| 301 | opts.minPressureThreshold = m_pressureThreshold->minThreshold(); |
| 302 | opts.maxPressureThreshold = m_pressureThreshold->maxThreshold(); |
| 303 | opts.minVelocityThreshold = m_velocityThreshold->minThreshold(); |
| 304 | opts.maxVelocityThreshold = m_velocityThreshold->maxThreshold(); |
| 305 | |
| 306 | return opts; |
| 307 | } |
| 308 | |
| 309 | void DynamicsPopup::(int i, bool state) |
| 310 | { |
| 311 | auto theme = SkinTheme::get(this); |
| 312 | m_dynamics->values() |
| 313 | ->getItem(i) |
| 314 | ->setIcon(state ? theme->parts.dropPixelsOk(): nullptr); |
| 315 | } |
| 316 | |
| 317 | bool DynamicsPopup::(int i) const |
| 318 | { |
| 319 | auto theme = SkinTheme::get(this); |
| 320 | return (m_dynamics->values() |
| 321 | ->getItem(i) |
| 322 | ->icon() == theme->parts.dropPixelsOk()); |
| 323 | } |
| 324 | |
| 325 | void DynamicsPopup::(ButtonSet::Item* item) |
| 326 | { |
| 327 | auto theme = SkinTheme::get(this); |
| 328 | const skin::SkinPartPtr& ok = theme->parts.dropPixelsOk(); |
| 329 | const int i = (item ? m_dynamics->values()->getItemIndex(item): -1); |
| 330 | |
| 331 | // Switch item off |
| 332 | if (item && item->icon().get() == ok.get()) { |
| 333 | item->setIcon(nullptr); |
| 334 | } |
| 335 | else { |
| 336 | switch (i) { |
| 337 | case SIZE_WITH_PRESSURE: |
| 338 | case SIZE_WITH_VELOCITY: |
| 339 | setCheck(SIZE_WITH_PRESSURE, i == SIZE_WITH_PRESSURE); |
| 340 | setCheck(SIZE_WITH_VELOCITY, i == SIZE_WITH_VELOCITY); |
| 341 | break; |
| 342 | case ANGLE_WITH_PRESSURE: |
| 343 | case ANGLE_WITH_VELOCITY: |
| 344 | setCheck(ANGLE_WITH_PRESSURE, i == ANGLE_WITH_PRESSURE); |
| 345 | setCheck(ANGLE_WITH_VELOCITY, i == ANGLE_WITH_VELOCITY); |
| 346 | break; |
| 347 | case GRADIENT_WITH_PRESSURE: |
| 348 | case GRADIENT_WITH_VELOCITY: |
| 349 | setCheck(GRADIENT_WITH_PRESSURE, i == GRADIENT_WITH_PRESSURE); |
| 350 | setCheck(GRADIENT_WITH_VELOCITY, i == GRADIENT_WITH_VELOCITY); |
| 351 | break; |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | const bool hasPressure = (isCheck(SIZE_WITH_PRESSURE) || |
| 356 | isCheck(ANGLE_WITH_PRESSURE) || |
| 357 | isCheck(GRADIENT_WITH_PRESSURE)); |
| 358 | const bool hasVelocity = (isCheck(SIZE_WITH_VELOCITY) || |
| 359 | isCheck(ANGLE_WITH_VELOCITY) || |
| 360 | isCheck(GRADIENT_WITH_VELOCITY)); |
| 361 | const bool needsSize = (isCheck(SIZE_WITH_PRESSURE) || |
| 362 | isCheck(SIZE_WITH_VELOCITY)); |
| 363 | const bool needsAngle = (isCheck(ANGLE_WITH_PRESSURE) || |
| 364 | isCheck(ANGLE_WITH_VELOCITY)); |
| 365 | const bool needsGradient = (isCheck(GRADIENT_WITH_PRESSURE) || |
| 366 | isCheck(GRADIENT_WITH_VELOCITY)); |
| 367 | const bool any = (needsSize || needsAngle || needsGradient); |
| 368 | doc::BrushRef brush = m_delegate->getActiveBrush(); |
| 369 | |
| 370 | if (needsSize && !m_dynamics->minSize()->isVisible()) { |
| 371 | m_dynamics->minSize()->setValue(1); |
| 372 | |
| 373 | int maxSize = brush->size(); |
| 374 | if (maxSize == 1) { |
| 375 | // If brush size == 1, we put it to 4 so the user has some size |
| 376 | // change by default. |
| 377 | maxSize = 4; |
| 378 | m_delegate->setMaxSize(maxSize); |
| 379 | } |
| 380 | m_dynamics->maxSize()->setValue(maxSize); |
| 381 | } |
| 382 | m_dynamics->sizeLabel()->setVisible(needsSize); |
| 383 | m_dynamics->minSize()->setVisible(needsSize); |
| 384 | m_dynamics->maxSize()->setVisible(needsSize); |
| 385 | |
| 386 | if (needsAngle && !m_dynamics->minAngle()->isVisible()) { |
| 387 | m_dynamics->minAngle()->setValue(brush->angle()); |
| 388 | m_dynamics->maxAngle()->setValue(brush->angle()); |
| 389 | } |
| 390 | m_dynamics->angleLabel()->setVisible(needsAngle); |
| 391 | m_dynamics->minAngle()->setVisible(needsAngle); |
| 392 | m_dynamics->maxAngle()->setVisible(needsAngle); |
| 393 | |
| 394 | m_dynamics->gradientLabel()->setVisible(needsGradient); |
| 395 | m_dynamics->gradientPlaceholder()->setVisible(needsGradient); |
| 396 | m_dynamics->gradientFromTo()->setVisible(needsGradient); |
| 397 | updateFromToText(); |
| 398 | |
| 399 | m_dynamics->separator()->setVisible(any); |
| 400 | m_dynamics->options()->setVisible(any); |
| 401 | m_dynamics->separator2()->setVisible(any); |
| 402 | m_dynamics->pressureLabel()->setVisible(hasPressure); |
| 403 | m_dynamics->pressurePlaceholder()->setVisible(hasPressure); |
| 404 | m_dynamics->velocityLabel()->setVisible(hasVelocity); |
| 405 | m_dynamics->velocityPlaceholder()->setVisible(hasVelocity); |
| 406 | |
| 407 | expandWindow(sizeHint()); |
| 408 | |
| 409 | m_hotRegion |= gfx::Region(boundsOnScreen()); |
| 410 | setHotRegion(m_hotRegion); |
| 411 | } |
| 412 | |
| 413 | void DynamicsPopup::() |
| 414 | { |
| 415 | m_dynamics->gradientFromTo()->setText( |
| 416 | m_fromTo == tools::ColorFromTo::BgToFg ? "BG > FG" : |
| 417 | m_fromTo == tools::ColorFromTo::FgToBg ? "FG > BG" : "-" ); |
| 418 | } |
| 419 | |
| 420 | void DynamicsPopup::() |
| 421 | { |
| 422 | doc::BrushRef brush = m_delegate->getActiveBrush(); |
| 423 | m_dynamics->maxSize()->setValue(brush->size()); |
| 424 | m_dynamics->maxAngle()->setValue(brush->angle()); |
| 425 | } |
| 426 | |
| 427 | bool DynamicsPopup::(Message* msg) |
| 428 | { |
| 429 | switch (msg->type()) { |
| 430 | |
| 431 | case kOpenMessage: |
| 432 | m_hotRegion = gfx::Region(boundsOnScreen()); |
| 433 | setHotRegion(m_hotRegion); |
| 434 | manager()->addMessageFilter(kMouseMoveMessage, this); |
| 435 | manager()->addMessageFilter(kMouseDownMessage, this); |
| 436 | disableFlags(IGNORE_MOUSE); |
| 437 | |
| 438 | updateWidgetsWithBrush(); |
| 439 | break; |
| 440 | |
| 441 | case kCloseMessage: |
| 442 | m_hotRegion.clear(); |
| 443 | manager()->removeMessageFilter(kMouseMoveMessage, this); |
| 444 | manager()->removeMessageFilter(kMouseDownMessage, this); |
| 445 | break; |
| 446 | |
| 447 | case kMouseEnterMessage: |
| 448 | m_velocity.reset(); |
| 449 | break; |
| 450 | |
| 451 | case kMouseMoveMessage: { |
| 452 | auto mouseMsg = static_cast<MouseMessage*>(msg); |
| 453 | |
| 454 | if (mouseMsg->pointerType() == PointerType::Pen || |
| 455 | mouseMsg->pointerType() == PointerType::Eraser) { |
| 456 | if (m_dynamics->pressurePlaceholder()->isVisible()) { |
| 457 | m_pressureThreshold->setSensorValue(mouseMsg->pressure()); |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | if (m_dynamics->velocityPlaceholder()->isVisible()) { |
| 462 | m_velocity.updateWithDisplayPoint(mouseMsg->position()); |
| 463 | |
| 464 | float v = m_velocity.velocity().magnitude() |
| 465 | / tools::VelocitySensor::kScreenPixelsForFullVelocity; |
| 466 | v = std::clamp(v, 0.0f, 1.0f); |
| 467 | |
| 468 | m_velocityThreshold->setSensorValue(v); |
| 469 | } |
| 470 | break; |
| 471 | } |
| 472 | |
| 473 | case kMouseDownMessage: { |
| 474 | if (!msg->display()) |
| 475 | break; |
| 476 | |
| 477 | auto mouseMsg = static_cast<const MouseMessage*>(msg); |
| 478 | auto screenPos = mouseMsg->screenPosition(); |
| 479 | auto picked = manager()->pickFromScreenPos(screenPos); |
| 480 | if ((picked == nullptr) || |
| 481 | (picked->window() != this && |
| 482 | picked->window() != m_ditheringSel->getWindowWidget())) { |
| 483 | closeWindow(nullptr); |
| 484 | } |
| 485 | break; |
| 486 | } |
| 487 | |
| 488 | } |
| 489 | return PopupWindow::onProcessMessage(msg); |
| 490 | } |
| 491 | |
| 492 | } // namespace app |
| 493 | |