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 | |