1 // Aseprite
2 // Copyright (C) 2001-2017  David Capello
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/editor/state_with_wheel_behavior.h"
12 
13 #include "app/app.h"
14 #include "app/commands/commands.h"
15 #include "app/commands/params.h"
16 #include "app/modules/palettes.h"
17 #include "app/pref/preferences.h"
18 #include "app/site.h"
19 #include "app/tools/active_tool.h"
20 #include "app/tools/tool_box.h"
21 #include "app/ui/color_bar.h"
22 #include "app/ui/editor/editor.h"
23 #include "app/ui/keyboard_shortcuts.h"
24 #include "app/ui/toolbar.h"
25 #include "app/ui_context.h"
26 #include "base/string.h"
27 #include "doc/layer.h"
28 #include "doc/palette.h"
29 #include "ui/message.h"
30 #include "ui/system.h"
31 #include "ui/theme.h"
32 
33 namespace app {
34 
35 using namespace ui;
36 
onMouseWheel(Editor * editor,MouseMessage * msg)37 bool StateWithWheelBehavior::onMouseWheel(Editor* editor, MouseMessage* msg)
38 {
39   gfx::Point delta = msg->wheelDelta();
40   double dz = delta.x + delta.y;
41   WheelAction wheelAction = WheelAction::None;
42   bool scrollBigSteps = false;
43 
44   if (KeyboardShortcuts::instance()->hasMouseWheelCustomization()) {
45     if (!Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel())
46       wheelAction = WheelAction::VScroll;
47     else
48       wheelAction = KeyboardShortcuts::instance()
49         ->getWheelActionFromMouseMessage(KeyContext::MouseWheel, msg);
50   }
51   // Default behavior
52   // TODO replace this code using KeyboardShortcuts::getDefaultMouseWheelTable()
53   else {
54     // Alt+mouse wheel changes the fg/bg colors
55     if (msg->altPressed()) {
56       if (msg->shiftPressed())
57         wheelAction = WheelAction::BgColor;
58       else
59         wheelAction = WheelAction::FgColor;
60     }
61     // Normal behavior: mouse wheel zooms If the message is from a
62     // precise wheel i.e. a trackpad/touch-like device, we scroll by
63     // default.
64     else if (Preferences::instance().editor.zoomWithWheel() && !msg->preciseWheel()) {
65       if (msg->ctrlPressed() && msg->shiftPressed())
66         wheelAction = WheelAction::Frame;
67       else if (msg->ctrlPressed())
68         wheelAction = WheelAction::BrushSize;
69       else if (delta.x != 0 || msg->shiftPressed())
70         wheelAction = WheelAction::HScroll;
71       else
72         wheelAction = WheelAction::Zoom;
73     }
74     // Zoom sliding two fingers
75     else if (Preferences::instance().editor.zoomWithSlide() && msg->preciseWheel()) {
76       if (msg->ctrlPressed() && msg->shiftPressed())
77         wheelAction = WheelAction::Frame;
78       else if (msg->ctrlPressed())
79         wheelAction = WheelAction::BrushSize;
80       else if (std::abs(delta.x) > std::abs(delta.y)) {
81         delta.y = 0;
82         dz = delta.x;
83         wheelAction = WheelAction::HScroll;
84       }
85       else if (msg->shiftPressed()) {
86         delta.x = 0;
87         dz = delta.y;
88         wheelAction = WheelAction::VScroll;
89       }
90       else {
91         delta.x = 0;
92         dz = delta.y;
93         wheelAction = WheelAction::Zoom;
94       }
95     }
96     // For laptops, it's convenient to that Ctrl+wheel zoom (because
97     // it's the "pinch" gesture).
98     else {
99       if (msg->ctrlPressed())
100         wheelAction = WheelAction::Zoom;
101       else if (delta.x != 0 || msg->shiftPressed())
102         wheelAction = WheelAction::HScroll;
103       else
104         wheelAction = WheelAction::VScroll;
105     }
106   }
107 
108   switch (wheelAction) {
109 
110     case WheelAction::None:
111       // Do nothing
112       break;
113 
114     case WheelAction::FgColor: {
115       int lastIndex = get_current_palette()->size()-1;
116       int newIndex = ColorBar::instance()->getFgColor().getIndex() + int(dz);
117       newIndex = MID(0, newIndex, lastIndex);
118       ColorBar::instance()->setFgColor(app::Color::fromIndex(newIndex));
119       break;
120     }
121 
122     case WheelAction::BgColor: {
123       int lastIndex = get_current_palette()->size()-1;
124       int newIndex = ColorBar::instance()->getBgColor().getIndex() + int(dz);
125       newIndex = MID(0, newIndex, lastIndex);
126       ColorBar::instance()->setBgColor(app::Color::fromIndex(newIndex));
127       break;
128     }
129 
130     case WheelAction::Frame: {
131       Command* command = nullptr;
132 
133       if (dz < 0.0)
134         command = Commands::instance()->byId(CommandId::GotoNextFrame());
135       else if (dz > 0.0)
136         command = Commands::instance()->byId(CommandId::GotoPreviousFrame());
137 
138       if (command)
139         UIContext::instance()->executeCommand(command);
140       break;
141     }
142 
143     case WheelAction::Zoom: {
144       render::Zoom zoom = editor->zoom();
145 
146       if (msg->preciseWheel()) {
147         dz /= 1.5;
148         if (dz < -1.0) dz = -1.0;
149         else if (dz > 1.0) dz = 1.0;
150       }
151 
152       zoom = render::Zoom::fromLinearScale(zoom.linearScale() - int(dz));
153 
154       setZoom(editor, zoom, msg->position());
155       break;
156     }
157 
158     case WheelAction::HScroll:
159     case WheelAction::VScroll: {
160       View* view = View::getView(editor);
161       gfx::Point scroll = view->viewScroll();
162 
163       if (!msg->preciseWheel()) {
164         gfx::Rect vp = view->viewportBounds();
165 
166         if (wheelAction == WheelAction::HScroll) {
167           delta.x = int(dz * vp.w);
168         }
169         else {
170           delta.y = int(dz * vp.h);
171         }
172 
173         if (scrollBigSteps) {
174           delta /= 2;
175         }
176         else {
177           delta /= 10;
178         }
179       }
180 
181       editor->setEditorScroll(scroll+delta);
182       break;
183     }
184 
185     case WheelAction::BrushSize: {
186       tools::Tool* tool = getActiveTool();
187       ToolPreferences::Brush& brush =
188         Preferences::instance().tool(tool).brush;
189 
190       brush.size(MID(doc::Brush::kMinBrushSize,
191                      brush.size()+dz,
192                      doc::Brush::kMaxBrushSize));
193       break;
194     }
195 
196     case WheelAction::BrushAngle: {
197       tools::Tool* tool = getActiveTool();
198       ToolPreferences::Brush& brush =
199         Preferences::instance().tool(tool).brush;
200 
201       int angle = brush.angle()+dz;
202       while (angle < 0)
203         angle += 180;
204       angle %= 181;
205 
206       brush.angle(MID(0, angle, 180));
207       break;
208     }
209 
210     case WheelAction::ToolSameGroup: {
211       tools::Tool* tool = getActiveTool();
212 
213       auto toolBox = App::instance()->toolBox();
214       std::vector<tools::Tool*> tools;
215       for (tools::Tool* t : *toolBox) {
216         if (tool->getGroup() == t->getGroup())
217           tools.push_back(t);
218       }
219 
220       auto begin = tools.begin();
221       auto end = tools.end();
222       auto it = std::find(begin, end, tool);
223       if (it != end) {
224         if (dz < 0) {
225           if (it == begin)
226             it = end;
227           --it;
228         }
229         else {
230           ++it;
231           if (it == end)
232             it = begin;
233         }
234         if (tool != *it)
235           ToolBar::instance()->selectTool(*it);
236       }
237       break;
238     }
239 
240     case WheelAction::ToolOtherGroup: {
241       tools::Tool* tool = getActiveTool();
242       auto toolBox = App::instance()->toolBox();
243       auto begin = toolBox->begin_group();
244       auto end = toolBox->end_group();
245       auto it = std::find(begin, end, tool->getGroup());
246       if (it != end) {
247         if (dz < 0) {
248           if (it == begin)
249             it = end;
250           --it;
251         }
252         else {
253           ++it;
254           if (it == end)
255             it = begin;
256         }
257         ToolBar::instance()->selectToolGroup(*it);
258       }
259       break;
260     }
261 
262     case WheelAction::Layer: {
263       Command* command = nullptr;
264       if (dz < 0.0)
265         command = Commands::instance()->byId(CommandId::GotoNextLayer());
266       else if (dz > 0.0)
267         command = Commands::instance()->byId(CommandId::GotoPreviousLayer());
268       if (command)
269         UIContext::instance()->executeCommand(command);
270       break;
271     }
272 
273     case WheelAction::InkOpacity: {
274       tools::Tool* tool = getActiveTool();
275       auto& toolPref = Preferences::instance().tool(tool);
276       int opacity = toolPref.opacity();
277       opacity = MID(0, opacity+dz*255/10, 255);
278       toolPref.opacity(opacity);
279       break;
280     }
281 
282     case WheelAction::LayerOpacity: {
283       Site site = UIContext::instance()->activeSite();
284       if (site.layer() &&
285           site.layer()->isImage() &&
286           site.layer()->isEditable()) {
287         Command* command = Commands::instance()->byId(CommandId::LayerOpacity());
288         if (command) {
289           int opacity = static_cast<doc::LayerImage*>(site.layer())->opacity();
290           opacity = MID(0, opacity+dz*255/10, 255);
291 
292           Params params;
293           params.set("opacity",
294                      base::convert_to<std::string>(opacity).c_str());
295           UIContext::instance()->executeCommand(command, params);
296         }
297       }
298       break;
299     }
300 
301     case WheelAction::CelOpacity: {
302       Site site = UIContext::instance()->activeSite();
303       if (site.layer() &&
304           site.layer()->isImage() &&
305           site.layer()->isEditable() &&
306           site.cel()) {
307         Command* command = Commands::instance()->byId(CommandId::CelOpacity());
308         if (command) {
309           int opacity = site.cel()->opacity();
310           opacity = MID(0, opacity+dz*255/10, 255);
311           Params params;
312           params.set("opacity",
313                      base::convert_to<std::string>(opacity).c_str());
314           UIContext::instance()->executeCommand(command, params);
315         }
316       }
317       break;
318     }
319 
320     case WheelAction::Alpha: {
321       disableQuickTool();
322 
323       ColorBar* colorBar = ColorBar::instance();
324       Color c = colorBar->getFgColor();
325       int a = c.getAlpha();
326       a = MID(0, a+dz*255/10, 255);
327       c.setAlpha(a);
328       colorBar->setFgColor(c);
329       break;
330     }
331 
332     case WheelAction::HslHue:
333     case WheelAction::HslSaturation:
334     case WheelAction::HslLightness: {
335       disableQuickTool();
336 
337       ColorBar* colorBar = ColorBar::instance();
338       Color c = colorBar->getFgColor();
339       double
340         h = c.getHslHue(),
341         s = c.getHslSaturation(),
342         l = c.getHslLightness();
343       switch (wheelAction) {
344         case WheelAction::HslHue:        h = h+dz*10.0; break;
345         case WheelAction::HslSaturation: s = s+dz/10.0; break;
346         case WheelAction::HslLightness:  l = l+dz/10.0; break;
347       }
348       colorBar->setFgColor(Color::fromHsl(MID(0.0, h, 360.0),
349                                           MID(0.0, s, 1.0),
350                                           MID(0.0, l, 1.0)));
351       break;
352     }
353 
354     case WheelAction::HsvHue:
355     case WheelAction::HsvSaturation:
356     case WheelAction::HsvValue: {
357       disableQuickTool();
358 
359       ColorBar* colorBar = ColorBar::instance();
360       Color c = colorBar->getFgColor();
361       double
362         h = c.getHsvHue(),
363         s = c.getHsvSaturation(),
364         v = c.getHsvValue();
365       switch (wheelAction) {
366         case WheelAction::HsvHue:        h = h+dz*10.0; break;
367         case WheelAction::HsvSaturation: s = s+dz/10.0; break;
368         case WheelAction::HsvValue:      v = v+dz/10.0; break;
369       }
370       colorBar->setFgColor(Color::fromHsv(MID(0.0, h, 360.0),
371                                           MID(0.0, s, 1.0),
372                                           MID(0.0, v, 1.0)));
373       break;
374     }
375 
376   }
377 
378   return true;
379 }
380 
onTouchMagnify(Editor * editor,ui::TouchMessage * msg)381 bool StateWithWheelBehavior::onTouchMagnify(Editor* editor, ui::TouchMessage* msg)
382 {
383   render::Zoom zoom = editor->zoom();
384   zoom = render::Zoom::fromScale(
385     zoom.internalScale() + zoom.internalScale() * msg->magnification());
386 
387   setZoom(editor, zoom, msg->position());
388   return true;
389 }
390 
setZoom(Editor * editor,const render::Zoom & zoom,const gfx::Point & mousePos)391 void StateWithWheelBehavior::setZoom(Editor* editor,
392                                      const render::Zoom& zoom,
393                                      const gfx::Point& mousePos)
394 {
395   bool center = Preferences::instance().editor.zoomFromCenterWithWheel();
396 
397   editor->setZoomAndCenterInMouse(
398     zoom, mousePos,
399     (center ? Editor::ZoomBehavior::CENTER:
400               Editor::ZoomBehavior::MOUSE));
401 }
402 
getActiveTool()403 tools::Tool* StateWithWheelBehavior::getActiveTool()
404 {
405   disableQuickTool();
406   return App::instance()->activeToolManager()->activeTool();
407 }
408 
disableQuickTool()409 void StateWithWheelBehavior::disableQuickTool()
410 {
411   auto atm = App::instance()->activeToolManager();
412   if (atm->quickTool()) {
413     // As Ctrl key could active the Move tool, and Ctrl+mouse wheel can
414     // change the size of the tool, we want to remove the quick tool so
415     // the effect is for the selected tool.
416     atm->newQuickToolSelectedFromEditor(nullptr);
417   }
418 }
419 
420 } // namespace app
421