1 | // Aseprite |
2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
3 | // Copyright (C) 2018 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/color_shades.h" |
13 | |
14 | #include "app/app.h" |
15 | #include "app/modules/gfx.h" |
16 | #include "app/modules/palettes.h" |
17 | #include "app/shade.h" |
18 | #include "app/ui/color_bar.h" |
19 | #include "app/ui/skin/skin_theme.h" |
20 | #include "doc/color_mode.h" |
21 | #include "doc/palette.h" |
22 | #include "doc/palette_picks.h" |
23 | #include "doc/remap.h" |
24 | #include "ui/graphics.h" |
25 | #include "ui/message.h" |
26 | #include "ui/paint_event.h" |
27 | #include "ui/size_hint_event.h" |
28 | #include "ui/system.h" |
29 | |
30 | #include <algorithm> |
31 | |
32 | namespace app { |
33 | |
34 | ColorShades::ColorShades(const Shade& colors, ClickType click) |
35 | : Widget(ui::kGenericWidget) |
36 | , m_click(click) |
37 | , m_shade(colors) |
38 | , m_minColors(1) |
39 | , m_hotIndex(-1) |
40 | , m_dragIndex(-1) |
41 | , m_boxSize(12) |
42 | { |
43 | setText("No colors" ); |
44 | initTheme(); |
45 | } |
46 | |
47 | void ColorShades::setMinColors(int minColors) |
48 | { |
49 | m_minColors = minColors; |
50 | invalidate(); |
51 | } |
52 | |
53 | void ColorShades::reverseShadeColors() |
54 | { |
55 | std::reverse(m_shade.begin(), m_shade.end()); |
56 | invalidate(); |
57 | } |
58 | |
59 | doc::Remap* ColorShades::createShadeRemap(bool left) |
60 | { |
61 | // We need two or more colors to create a shading remap. In |
62 | // other case, the ShadingInkProcessing will use the full |
63 | // color palette. |
64 | Shade colors = getShade(); |
65 | if (colors.size() <= 1) |
66 | return nullptr; |
67 | |
68 | std::unique_ptr<doc::Remap> remap( |
69 | new doc::Remap(get_current_palette()->size())); |
70 | |
71 | for (int i=0; i<remap->size(); ++i) |
72 | remap->map(i, i); |
73 | |
74 | if (left) { |
75 | for (int i=1; i<int(colors.size()); ++i) { |
76 | int j = colors[i].getIndex(); |
77 | if (j >= 0 && j < remap->size()) |
78 | remap->map(j, colors[i-1].getIndex()); |
79 | } |
80 | } |
81 | else { |
82 | for (int i=0; i<int(colors.size())-1; ++i) { |
83 | int j = colors[i].getIndex(); |
84 | if (j >= 0 && j < remap->size()) |
85 | remap->map(j, colors[i+1].getIndex()); |
86 | } |
87 | } |
88 | return remap.release(); |
89 | } |
90 | |
91 | void ColorShades::setShade(const Shade& shade) |
92 | { |
93 | m_shade = shade; |
94 | invalidate(); |
95 | parent()->parent()->layout(); |
96 | } |
97 | |
98 | void ColorShades::onInitTheme(ui::InitThemeEvent& ev) |
99 | { |
100 | Widget::onInitTheme(ev); |
101 | |
102 | auto theme = skin::SkinTheme::get(this); |
103 | |
104 | switch (m_click) { |
105 | case ClickEntries: |
106 | case DragAndDropEntries: |
107 | setStyle(theme->styles.normalShadeView()); |
108 | break; |
109 | case ClickWholeShade: |
110 | setStyle(theme->styles.menuShadeView()); |
111 | break; |
112 | } |
113 | } |
114 | |
115 | bool ColorShades::onProcessMessage(ui::Message* msg) |
116 | { |
117 | switch (msg->type()) { |
118 | |
119 | case ui::kSetCursorMessage: |
120 | if (hasCapture()) { |
121 | ui::set_mouse_cursor(ui::kMoveCursor); |
122 | return true; |
123 | } |
124 | else if (m_click == ClickEntries && |
125 | m_hotIndex >= 0 && |
126 | m_hotIndex < int(m_shade.size())) { |
127 | ui::set_mouse_cursor(ui::kHandCursor); |
128 | return true; |
129 | } |
130 | break; |
131 | |
132 | case ui::kMouseEnterMessage: |
133 | case ui::kMouseLeaveMessage: |
134 | if (!hasCapture()) |
135 | m_hotIndex = -1; |
136 | |
137 | invalidate(); |
138 | break; |
139 | |
140 | case ui::kMouseDownMessage: |
141 | if (m_hotIndex >= 0 && |
142 | m_hotIndex < int(m_shade.size())) { |
143 | switch (m_click) { |
144 | case ClickEntries: { |
145 | ClickEvent ev(static_cast<ui::MouseMessage*>(msg)->button()); |
146 | Click(ev); |
147 | |
148 | m_hotIndex = -1; |
149 | invalidate(); |
150 | break; |
151 | } |
152 | case DragAndDropEntries: |
153 | m_dragIndex = m_hotIndex; |
154 | m_dropBefore = false; |
155 | captureMouse(); |
156 | break; |
157 | } |
158 | } |
159 | break; |
160 | |
161 | case ui::kMouseUpMessage: { |
162 | auto button = static_cast<ui::MouseMessage*>(msg)->button(); |
163 | |
164 | if (m_click == ClickWholeShade) { |
165 | setSelected(true); |
166 | |
167 | ClickEvent ev(button); |
168 | Click(ev); |
169 | |
170 | closeWindow(); |
171 | } |
172 | |
173 | if (m_dragIndex >= 0) { |
174 | ASSERT(m_dragIndex < int(m_shade.size())); |
175 | |
176 | auto color = m_shade[m_dragIndex]; |
177 | m_shade.erase(m_shade.begin()+m_dragIndex); |
178 | if (m_hotIndex >= 0) |
179 | m_shade.insert(m_shade.begin()+m_hotIndex, color); |
180 | |
181 | m_dragIndex = -1; |
182 | invalidate(); |
183 | |
184 | ClickEvent ev(button); |
185 | Click(ev); |
186 | |
187 | // Relayout the context bar if we have removed an entry. |
188 | // |
189 | // TODO it looks like this should be handled in some kind of |
190 | // Change() event in the ColorBar |
191 | if (m_hotIndex < 0 && |
192 | parent() && |
193 | parent()->parent()) { |
194 | parent()->parent()->layout(); |
195 | } |
196 | } |
197 | |
198 | if (hasCapture()) |
199 | releaseMouse(); |
200 | break; |
201 | } |
202 | |
203 | case ui::kMouseMoveMessage: { |
204 | ui::MouseMessage* mouseMsg = static_cast<ui::MouseMessage*>(msg); |
205 | gfx::Point mousePos = mouseMsg->position() - bounds().origin(); |
206 | gfx::Rect bounds = clientBounds(); |
207 | int hot = -1; |
208 | |
209 | bounds.shrink(3*ui::guiscale()); |
210 | |
211 | if (bounds.contains(mousePos)) { |
212 | int count = std::max(1, size()); |
213 | int boxWidth = std::max(1, bounds.w / count); |
214 | hot = (mousePos.x - bounds.x) / boxWidth; |
215 | hot = std::clamp(hot, 0, count-1); |
216 | } |
217 | |
218 | if (m_hotIndex != hot) { |
219 | m_hotIndex = hot; |
220 | invalidate(); |
221 | } |
222 | |
223 | bool dropBefore = |
224 | (hot >= 0 && mousePos.x < (bounds.x+m_boxSize*ui::guiscale()*hot)+m_boxSize*ui::guiscale()/2); |
225 | if (m_dropBefore != dropBefore) { |
226 | m_dropBefore = dropBefore; |
227 | invalidate(); |
228 | } |
229 | break; |
230 | } |
231 | } |
232 | return Widget::onProcessMessage(msg); |
233 | } |
234 | |
235 | void ColorShades::onSizeHint(ui::SizeHintEvent& ev) |
236 | { |
237 | int size = this->size(); |
238 | if (size < 2) |
239 | ev.setSizeHint(gfx::Size((16+m_boxSize)*ui::guiscale()+textWidth(), 18*ui::guiscale())); |
240 | else { |
241 | if (m_click == ClickWholeShade && size > 16) |
242 | size = 16; |
243 | ev.setSizeHint(gfx::Size(6+m_boxSize*size, 18)*ui::guiscale()); |
244 | } |
245 | } |
246 | |
247 | void ColorShades::onPaint(ui::PaintEvent& ev) |
248 | { |
249 | auto theme = skin::SkinTheme::get(this); |
250 | ui::Graphics* g = ev.graphics(); |
251 | gfx::Rect bounds = clientBounds(); |
252 | |
253 | theme->paintWidget(g, this, style(), bounds); |
254 | |
255 | bounds.shrink(3*ui::guiscale()); |
256 | |
257 | Shade colors = getShade(); |
258 | if (colors.size() >= m_minColors) { |
259 | gfx::Rect box(bounds.x, bounds.y, |
260 | bounds.w / std::max(1, int(colors.size())), |
261 | bounds.h); |
262 | gfx::Rect hotBounds; |
263 | |
264 | int j = 0; |
265 | for (int i=0; box.x<bounds.x2(); ++i, box.x += box.w) { |
266 | // Make the last box a little bigger to just use all |
267 | // available size |
268 | if (i == int(colors.size())-1) |
269 | box.w = bounds.x2()-box.x; |
270 | |
271 | app::Color color; |
272 | |
273 | if (m_dragIndex >= 0 && |
274 | m_hotIndex == i) { |
275 | color = colors[m_dragIndex]; |
276 | } |
277 | else { |
278 | if (j == m_dragIndex) { |
279 | ++j; |
280 | } |
281 | if (j < int(colors.size())) |
282 | color = colors[j++]; |
283 | else |
284 | color = app::Color::fromMask(); |
285 | } |
286 | |
287 | draw_color(g, box, color, |
288 | (doc::ColorMode)app_get_current_pixel_format()); |
289 | |
290 | if (m_hotIndex == i) |
291 | hotBounds = box; |
292 | } |
293 | |
294 | if (!hotBounds.isEmpty() && |
295 | isHotEntryVisible()) { |
296 | hotBounds.enlarge(3*ui::guiscale()); |
297 | |
298 | ui::PaintWidgetPartInfo info; |
299 | theme->paintWidgetPart( |
300 | g, theme->styles.shadeSelection(), hotBounds, info); |
301 | } |
302 | } |
303 | else { |
304 | g->fillRect(theme->colors.editorFace(), bounds); |
305 | g->drawAlignedUIText(text(), theme->colors.face(), gfx::ColorNone, bounds, |
306 | ui::CENTER | ui::MIDDLE); |
307 | } |
308 | } |
309 | |
310 | } // namespace app |
311 | |