1// Aseprite
2// Copyright (C) 2020-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/editor/state_with_wheel_behavior.h"
13
14#include "app/app.h"
15#include "app/commands/commands.h"
16#include "app/commands/params.h"
17#include "app/modules/palettes.h"
18#include "app/pref/preferences.h"
19#include "app/site.h"
20#include "app/tools/active_tool.h"
21#include "app/tools/tool_box.h"
22#include "app/ui/color_bar.h"
23#include "app/ui/context_bar.h"
24#include "app/ui/editor/editor.h"
25#include "app/ui/keyboard_shortcuts.h"
26#include "app/ui/toolbar.h"
27#include "app/ui_context.h"
28#include "base/string.h"
29#include "doc/brush.h"
30#include "doc/layer.h"
31#include "doc/palette.h"
32#include "ui/message.h"
33#include "ui/system.h"
34#include "ui/theme.h"
35
36#include "app/tools/ink.h"
37#include "app/ui/skin/skin_theme.h"
38
39namespace app {
40
41using namespace ui;
42using PreciseWheel = StateWithWheelBehavior::PreciseWheel;
43
44template<typename T>
45static inline void adjust_value(PreciseWheel preciseWheel, double dz, T& v, T min, T max)
46{
47 if (preciseWheel == PreciseWheel::On)
48 v = std::clamp<T>(T(v+dz), min, max);
49 else
50 v = std::clamp<T>(T(v+dz*max/T(10)), min, max);
51}
52
53template<typename T>
54static inline void adjust_hue(PreciseWheel preciseWheel, double dz, T& v, T min, T max)
55{
56 if (preciseWheel == PreciseWheel::On)
57 v = std::clamp<T>(T(v+dz), min, max);
58 else
59 v = std::clamp<T>(T(v+dz*T(10)), min, max);
60}
61
62static inline void adjust_unit(PreciseWheel preciseWheel, double dz, double& v)
63{
64 v = std::clamp<double>(v+(preciseWheel == PreciseWheel::On ? dz/100.0: dz/25.0), 0.0, 1.0);
65}
66
67StateWithWheelBehavior::StateWithWheelBehavior()
68{
69}
70
71bool StateWithWheelBehavior::onMouseWheel(Editor* editor, MouseMessage* msg)
72{
73 gfx::Point delta = msg->wheelDelta();
74 double dz = delta.x + delta.y;
75 WheelAction wheelAction = WheelAction::None;
76
77 if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
78 if (!Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel())
79 wheelAction = WheelAction::VScroll;
80 else
81 wheelAction = KeyboardShortcuts::instance()
82 ->getWheelActionFromMouseMessage(KeyContext::MouseWheel, msg);
83 }
84 // Default behavior
85 // TODO replace this code using KeyboardShortcuts::getDefaultMouseWheelTable()
86 else {
87 // Alt+mouse wheel changes the fg/bg colors
88 if (msg->altPressed()) {
89 bool tilemapMode = (editor->getSite().tilemapMode() == TilemapMode::Tiles);
90 if (msg->shiftPressed())
91 wheelAction = (tilemapMode ? WheelAction::BgTile : WheelAction::BgColor);
92 else
93 wheelAction = (tilemapMode ? WheelAction::FgTile : WheelAction::FgColor);
94 }
95 // Normal behavior: mouse wheel zooms If the message is from a
96 // precise wheel i.e. a trackpad/touch-like device, we scroll by
97 // default.
98 else if (Preferences::instance().editor.zoomWithWheel() && !msg->preciseWheel()) {
99 if (msg->ctrlPressed() && msg->shiftPressed())
100 wheelAction = WheelAction::Frame;
101 else if (msg->ctrlPressed())
102 wheelAction = WheelAction::BrushSize;
103 else if (delta.x != 0 || msg->shiftPressed())
104 wheelAction = WheelAction::HScroll;
105 else
106 wheelAction = WheelAction::Zoom;
107 }
108 // Zoom sliding two fingers
109 else if (Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel()) {
110 if (msg->ctrlPressed() && msg->shiftPressed())
111 wheelAction = WheelAction::Frame;
112 else if (msg->ctrlPressed())
113 wheelAction = WheelAction::BrushSize;
114 else if (std::abs(delta.x) > std::abs(delta.y)) {
115 delta.y = 0;
116 dz = delta.x;
117 wheelAction = WheelAction::HScroll;
118 }
119 else if (msg->shiftPressed()) {
120 delta.x = 0;
121 dz = delta.y;
122 wheelAction = WheelAction::VScroll;
123 }
124 else {
125 delta.x = 0;
126 dz = delta.y;
127 wheelAction = WheelAction::Zoom;
128 }
129 }
130 // For laptops, it's convenient to that Ctrl+wheel zoom (because
131 // it's the "pinch" gesture).
132 else {
133 if (msg->ctrlPressed())
134 wheelAction = WheelAction::Zoom;
135 else if (delta.x != 0 || msg->shiftPressed())
136 wheelAction = WheelAction::HScroll;
137 else
138 wheelAction = WheelAction::VScroll;
139 }
140 }
141
142 processWheelAction(editor,
143 wheelAction,
144 msg->position(),
145 delta,
146 dz,
147 // The possibility for big scroll steps was lost
148 // in history (it was possible using Shift key in
149 // very old versions, now Shift is used for
150 // horizontal scroll).
151 ScrollBigSteps::Off,
152 (msg->preciseWheel() ?
153 PreciseWheel::On:
154 PreciseWheel::Off),
155 FromMouseWheel::On);
156 return true;
157}
158
159void StateWithWheelBehavior::processWheelAction(
160 Editor* editor,
161 WheelAction wheelAction,
162 const gfx::Point& position,
163 gfx::Point delta,
164 double dz,
165 const ScrollBigSteps scrollBigSteps,
166 const PreciseWheel preciseWheel,
167 const FromMouseWheel fromMouseWheel)
168{
169 switch (wheelAction) {
170
171 case WheelAction::None:
172 // Do nothing
173 break;
174
175 case WheelAction::FgColor: {
176 int lastIndex = get_current_palette()->size()-1;
177 int newIndex = initialFgColor().getIndex() + int(dz);
178 newIndex = std::clamp(newIndex, 0, lastIndex);
179 changeFgColor(app::Color::fromIndex(newIndex));
180 break;
181 }
182
183 case WheelAction::BgColor: {
184 int lastIndex = get_current_palette()->size()-1;
185 int newIndex = initialBgColor().getIndex() + int(dz);
186 newIndex = std::clamp(newIndex, 0, lastIndex);
187 ColorBar::instance()->setBgColor(app::Color::fromIndex(newIndex));
188 break;
189 }
190
191 case WheelAction::FgTile: {
192 auto tilesView = ColorBar::instance()->getTilesView();
193 int lastIndex = tilesView->tileset()->size()-1;
194 int newIndex = initialFgTileIndex() + int(dz);
195 newIndex = std::clamp(newIndex, 0, lastIndex);
196 ColorBar::instance()->setFgTile(newIndex);
197 break;
198 }
199
200 case WheelAction::BgTile: {
201 auto tilesView = ColorBar::instance()->getTilesView();
202 int lastIndex = tilesView->tileset()->size()-1;
203 int newIndex = initialBgTileIndex() + int(dz);
204 newIndex = std::clamp(newIndex, 0, lastIndex);
205 ColorBar::instance()->setBgTile(newIndex);
206 break;
207 }
208
209 case WheelAction::Frame: {
210 frame_t deltaFrame = 0;
211 if (preciseWheel == PreciseWheel::On) {
212 if (dz < 0.0)
213 deltaFrame = +1;
214 else if (dz > 0.0)
215 deltaFrame = -1;
216 }
217 else {
218 deltaFrame = -dz;
219 }
220
221 frame_t frame = initialFrame(editor) + deltaFrame;
222 frame_t nframes = editor->sprite()->totalFrames();
223 while (frame < 0)
224 frame += nframes;
225 while (frame >= nframes)
226 frame -= nframes;
227
228 editor->setFrame(frame);
229 break;
230 }
231
232 case WheelAction::Zoom: {
233 render::Zoom zoom = initialZoom(editor);
234
235 if (preciseWheel == PreciseWheel::On) {
236 dz /= 1.5;
237 if (dz < -1.0) dz = -1.0;
238 else if (dz > 1.0) dz = 1.0;
239 }
240
241 zoom = render::Zoom::fromLinearScale(zoom.linearScale() - int(dz));
242
243 setZoom(editor, zoom, position);
244 break;
245 }
246
247 case WheelAction::HScroll:
248 case WheelAction::VScroll: {
249 View* view = View::getView(editor);
250 gfx::Point scroll = initialScroll(editor);
251
252 if (preciseWheel == PreciseWheel::Off) {
253 gfx::Rect vp = view->viewportBounds();
254
255 if (wheelAction == WheelAction::HScroll) {
256 delta.x = int(dz * vp.w);
257 }
258 else {
259 delta.y = int(dz * vp.h);
260 }
261
262 if (scrollBigSteps == ScrollBigSteps::On) {
263 delta /= 2;
264 }
265 else {
266 delta /= 10;
267 }
268 }
269
270 editor->setEditorScroll(scroll+delta);
271 break;
272 }
273
274 case WheelAction::BrushSize: {
275 tools::Tool* tool = getActiveTool();
276 ToolPreferences::Brush& brush =
277 Preferences::instance().tool(tool).brush;
278
279 if (fromMouseWheel == FromMouseWheel::On) {
280#if LAF_WINDOWS || LAF_LINUX
281 // By default on macOS the mouse wheel is correct, up increase
282 // brush size, and down decrease it. But on Windows and Linux
283 // it's inverted.
284 dz = -dz;
285#endif
286
287 // We can configure the mouse wheel for brush size to behave as
288 // in previous versions.
289 if (Preferences::instance().editor.invertBrushSizeWheel())
290 dz = -dz;
291 }
292
293 brush.size(
294 std::clamp(
295 int(initialBrushSize()+dz),
296 // If we use the "static const int" member directly here,
297 // we'll get a linker error (when compiling without
298 // optimizations) because we should need to define the
299 // address of these constants in "doc/brush.cpp"
300 int(doc::Brush::kMinBrushSize),
301 int(doc::Brush::kMaxBrushSize)));
302 break;
303 }
304
305 case WheelAction::BrushAngle: {
306 tools::Tool* tool = getActiveTool();
307 ToolPreferences::Brush& brush =
308 Preferences::instance().tool(tool).brush;
309
310 int angle = initialBrushAngle()+dz;
311 while (angle < 0)
312 angle += 180;
313 angle %= 181;
314
315 brush.angle(std::clamp(angle, 0, 180));
316 break;
317 }
318
319 case WheelAction::ToolSameGroup: {
320 const tools::Tool* tool = getInitialToolInActiveGroup();
321
322 auto toolBox = App::instance()->toolBox();
323 std::vector<tools::Tool*> tools;
324 for (tools::Tool* t : *toolBox) {
325 if (tool->getGroup() == t->getGroup())
326 tools.push_back(t);
327 }
328
329 auto begin = tools.begin();
330 auto end = tools.end();
331 auto it = std::find(begin, end, tool);
332 if (it != end) {
333 int i = std::round(dz);
334 while (i < 0) {
335 ++i;
336 if (it == begin)
337 it = end;
338 --it;
339 }
340 while (i > 0) {
341 --i;
342 ++it;
343 if (it == end)
344 it = begin;
345 }
346 onToolChange(*it);
347 }
348 break;
349 }
350
351 case WheelAction::ToolOtherGroup: {
352 const tools::Tool* tool = initialTool();
353
354 auto toolBox = App::instance()->toolBox();
355 auto begin = toolBox->begin_group();
356 auto end = toolBox->end_group();
357 auto it = std::find(begin, end, tool->getGroup());
358 if (it != end) {
359 int i = std::round(dz);
360 while (i < 0) {
361 ++i;
362 if (it == begin)
363 it = end;
364 --it;
365 }
366 while (i > 0) {
367 --i;
368 ++it;
369 if (it == end)
370 it = begin;
371 }
372 onToolGroupChange(editor, *it);
373 }
374 break;
375 }
376
377 case WheelAction::Layer: {
378 int deltaLayer = 0;
379 if (preciseWheel == PreciseWheel::On) {
380 if (dz < 0.0)
381 deltaLayer = +1;
382 else if (dz > 0.0)
383 deltaLayer = -1;
384 }
385 else {
386 deltaLayer = -dz;
387 }
388
389 const LayerList& layers = browsableLayers(editor);
390 layer_t layer = initialLayer(editor) + deltaLayer;
391 layer_t nlayers = layers.size();
392 while (layer < 0)
393 layer += nlayers;
394 while (layer >= nlayers)
395 layer -= nlayers;
396
397 editor->setLayer(layers[layer]);
398 break;
399 }
400
401 case WheelAction::InkType: {
402 int ink = int(initialInkType(editor));
403 int deltaInk = int(dz);
404 if (preciseWheel == PreciseWheel::On) {
405 if (dz < 0.0)
406 deltaInk = +1;
407 else if (dz > 0.0)
408 deltaInk = -1;
409 }
410 ink += deltaInk;
411 ink = std::clamp(ink,
412 int(tools::InkType::FIRST),
413 int(tools::InkType::LAST));
414
415 App::instance()->contextBar()->setInkType(tools::InkType(ink));
416 break;
417 }
418
419 case WheelAction::InkOpacity: {
420 int opacity = initialInkOpacity(editor);
421 adjust_value(preciseWheel, dz, opacity, 0, 255);
422
423 tools::Tool* tool = getActiveTool();
424 auto& toolPref = Preferences::instance().tool(tool);
425 toolPref.opacity(opacity);
426 break;
427 }
428
429 case WheelAction::LayerOpacity: {
430 Site site = UIContext::instance()->activeSite();
431 if (site.layer() &&
432 site.layer()->isImage() &&
433 site.layer()->isEditable()) {
434 Command* command = Commands::instance()->byId(CommandId::LayerOpacity());
435 if (command) {
436 int opacity = initialLayerOpacity(editor);
437 adjust_value(preciseWheel, dz, opacity, 0, 255);
438
439 Params params;
440 params.set("opacity",
441 base::convert_to<std::string>(opacity).c_str());
442 UIContext::instance()->executeCommand(command, params);
443 }
444 }
445 break;
446 }
447
448 case WheelAction::CelOpacity: {
449 Site site = UIContext::instance()->activeSite();
450 if (site.layer() &&
451 site.layer()->isImage() &&
452 site.layer()->isEditable() &&
453 site.cel()) {
454 Command* command = Commands::instance()->byId(CommandId::CelOpacity());
455 if (command) {
456 int opacity = initialCelOpacity(editor);
457 adjust_value(preciseWheel, dz, opacity, 0, 255);
458
459 Params params;
460 params.set("opacity",
461 base::convert_to<std::string>(opacity).c_str());
462 UIContext::instance()->executeCommand(command, params);
463 }
464 }
465 break;
466 }
467
468 case WheelAction::Alpha: {
469 disableQuickTool();
470
471 Color c = initialFgColor();
472 int a = c.getAlpha();
473 adjust_value(preciseWheel, dz, a, 0, 255);
474 c.setAlpha(a);
475
476 changeFgColor(c);
477 break;
478 }
479
480 case WheelAction::HslHue:
481 case WheelAction::HslSaturation:
482 case WheelAction::HslLightness: {
483 disableQuickTool();
484
485 Color c = initialFgColor();
486 double
487 h = c.getHslHue(),
488 s = c.getHslSaturation(),
489 l = c.getHslLightness();
490 switch (wheelAction) {
491 case WheelAction::HslHue:
492 adjust_hue(preciseWheel, dz, h, 0.0, 360.0);
493 break;
494 case WheelAction::HslSaturation:
495 adjust_unit(preciseWheel, dz, s);
496 break;
497 case WheelAction::HslLightness:
498 adjust_unit(preciseWheel, dz, l);
499 break;
500 }
501
502 changeFgColor(Color::fromHsl(std::clamp(h, 0.0, 360.0),
503 std::clamp(s, 0.0, 1.0),
504 std::clamp(l, 0.0, 1.0)));
505 break;
506 }
507
508 case WheelAction::HsvHue:
509 case WheelAction::HsvSaturation:
510 case WheelAction::HsvValue: {
511 disableQuickTool();
512
513 Color c = initialFgColor();
514 double
515 h = c.getHsvHue(),
516 s = c.getHsvSaturation(),
517 v = c.getHsvValue();
518 switch (wheelAction) {
519 case WheelAction::HsvHue:
520 adjust_hue(preciseWheel, dz, h, 0.0, 360.0);
521 break;
522 case WheelAction::HsvSaturation:
523 adjust_unit(preciseWheel, dz, s);
524 break;
525 case WheelAction::HsvValue:
526 adjust_unit(preciseWheel, dz, v);
527 break;
528 }
529
530 changeFgColor(Color::fromHsv(std::clamp(h, 0.0, 360.0),
531 std::clamp(s, 0.0, 1.0),
532 std::clamp(v, 0.0, 1.0)));
533 break;
534 }
535
536 }
537}
538
539bool StateWithWheelBehavior::onTouchMagnify(Editor* editor, ui::TouchMessage* msg)
540{
541 render::Zoom zoom = editor->zoom();
542 zoom = render::Zoom::fromScale(
543 zoom.internalScale() + zoom.internalScale() * msg->magnification());
544
545 setZoom(editor, zoom, msg->position());
546 return true;
547}
548
549bool StateWithWheelBehavior::onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos)
550{
551 tools::Ink* ink = editor->getCurrentEditorInk();
552 auto theme = skin::SkinTheme::get(editor);
553
554 if (ink) {
555 // If the current tool change selection (e.g. rectangular marquee, etc.)
556 if (ink->isSelection()) {
557 editor->showBrushPreview(mouseScreenPos);
558 return true;
559 }
560 else if (ink->isEyedropper()) {
561 editor->showMouseCursor(
562 kCustomCursor, theme->cursors.eyedropper());
563 return true;
564 }
565 else if (ink->isZoom()) {
566 editor->showMouseCursor(
567 kCustomCursor, theme->cursors.magnifier());
568 return true;
569 }
570 else if (ink->isScrollMovement()) {
571 editor->showMouseCursor(kScrollCursor);
572 return true;
573 }
574 else if (ink->isCelMovement()) {
575 editor->showMouseCursor(kMoveCursor);
576 return true;
577 }
578 }
579
580 // Draw
581 if (editor->canDraw()) {
582 editor->showBrushPreview(mouseScreenPos);
583 }
584 // Forbidden
585 else {
586 editor->showMouseCursor(kForbiddenCursor);
587 }
588
589 return true;
590}
591
592void StateWithWheelBehavior::setZoom(Editor* editor,
593 const render::Zoom& zoom,
594 const gfx::Point& mousePos)
595{
596 bool center = Preferences::instance().editor.zoomFromCenterWithWheel();
597
598 editor->setZoomAndCenterInMouse(
599 zoom, mousePos,
600 (center ? Editor::ZoomBehavior::CENTER:
601 Editor::ZoomBehavior::MOUSE));
602}
603
604Color StateWithWheelBehavior::initialFgColor() const
605{
606 return ColorBar::instance()->getFgColor();
607}
608
609Color StateWithWheelBehavior::initialBgColor() const
610{
611 return ColorBar::instance()->getBgColor();
612}
613
614int StateWithWheelBehavior::initialFgTileIndex() const
615{
616 return ColorBar::instance()->getFgTile();
617}
618
619int StateWithWheelBehavior::initialBgTileIndex() const
620{
621 return ColorBar::instance()->getBgTile();
622}
623
624int StateWithWheelBehavior::initialBrushSize()
625{
626 tools::Tool* tool = getActiveTool();
627 ToolPreferences::Brush& brush =
628 Preferences::instance().tool(tool).brush;
629
630 return brush.size();
631}
632
633int StateWithWheelBehavior::initialBrushAngle()
634{
635 tools::Tool* tool = getActiveTool();
636 ToolPreferences::Brush& brush =
637 Preferences::instance().tool(tool).brush;
638
639 return brush.angle();
640}
641
642gfx::Point StateWithWheelBehavior::initialScroll(Editor* editor) const
643{
644 View* view = View::getView(editor);
645 return view->viewScroll();
646}
647
648render::Zoom StateWithWheelBehavior::initialZoom(Editor* editor) const
649{
650 return editor->zoom();
651}
652
653doc::frame_t StateWithWheelBehavior::initialFrame(Editor* editor) const
654{
655 return editor->frame();
656}
657
658doc::layer_t StateWithWheelBehavior::initialLayer(Editor* editor) const
659{
660 return doc::find_layer_index(browsableLayers(editor), editor->layer());
661}
662
663tools::InkType StateWithWheelBehavior::initialInkType(Editor* editor) const
664{
665 tools::Tool* tool = getActiveTool();
666 auto& toolPref = Preferences::instance().tool(tool);
667 return toolPref.ink();
668}
669
670int StateWithWheelBehavior::initialInkOpacity(Editor* editor) const
671{
672 tools::Tool* tool = getActiveTool();
673 auto& toolPref = Preferences::instance().tool(tool);
674 return toolPref.opacity();
675}
676
677int StateWithWheelBehavior::initialCelOpacity(Editor* editor) const
678{
679 doc::Layer* layer = editor->layer();
680 if (layer &&
681 layer->isImage() &&
682 layer->isEditable()) {
683 if (Cel* cel = layer->cel(editor->frame()))
684 return cel->opacity();
685 }
686 return 0;
687}
688
689int StateWithWheelBehavior::initialLayerOpacity(Editor* editor) const
690{
691 doc::Layer* layer = editor->layer();
692 if (layer &&
693 layer->isImage() &&
694 layer->isEditable()) {
695 return static_cast<doc::LayerImage*>(layer)->opacity();
696 }
697 else
698 return 0;
699}
700
701tools::Tool* StateWithWheelBehavior::initialTool() const
702{
703 return getActiveTool();
704}
705
706void StateWithWheelBehavior::changeFgColor(Color c)
707{
708 ColorBar::instance()->setFgColor(c);
709}
710
711tools::Tool* StateWithWheelBehavior::getActiveTool() const
712{
713 disableQuickTool();
714 return App::instance()->activeToolManager()->activeTool();
715}
716
717const doc::LayerList& StateWithWheelBehavior::browsableLayers(Editor* editor) const
718{
719 if (m_browsableLayers.empty())
720 m_browsableLayers = editor->sprite()->allBrowsableLayers();
721 return m_browsableLayers;
722}
723
724void StateWithWheelBehavior::disableQuickTool() const
725{
726 auto atm = App::instance()->activeToolManager();
727 if (atm->quickTool()) {
728 // As Ctrl key could active the Move tool, and Ctrl+mouse wheel can
729 // change the size of the tool, we want to remove the quick tool so
730 // the effect is for the selected tool.
731 atm->newQuickToolSelectedFromEditor(nullptr);
732 }
733}
734
735tools::Tool* StateWithWheelBehavior::getInitialToolInActiveGroup()
736{
737 if (!m_tool)
738 m_tool = initialTool();
739 return m_tool;
740}
741
742void StateWithWheelBehavior::onToolChange(tools::Tool* tool)
743{
744 ToolBar::instance()->selectTool(tool);
745 m_tool = tool;
746}
747
748void StateWithWheelBehavior::onToolGroupChange(Editor* editor,
749 tools::ToolGroup* group)
750{
751 ToolBar::instance()->selectToolGroup(group);
752
753 // Update initial tool in active group as the group has just
754 // changed. Useful when the same key modifiers are associated to
755 // WheelAction::ToolSameGroup and WheelAction::ToolOtherGroup at the
756 // same time.
757 m_tool = getActiveTool();
758}
759
760} // namespace app
761