1 | // Aseprite |
2 | // Copyright (C) 2020-2022 Igara Studio S.A. |
3 | // Copyright (C) 2001-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_button.h" |
13 | |
14 | #include "app/app.h" |
15 | #include "app/color.h" |
16 | #include "app/color_utils.h" |
17 | #include "app/modules/editors.h" |
18 | #include "app/modules/gfx.h" |
19 | #include "app/site.h" |
20 | #include "app/ui/color_bar.h" |
21 | #include "app/ui/color_popup.h" |
22 | #include "app/ui/editor/editor.h" |
23 | #include "app/ui/skin/skin_theme.h" |
24 | #include "app/ui/status_bar.h" |
25 | #include "app/ui_context.h" |
26 | #include "doc/layer.h" |
27 | #include "doc/sprite.h" |
28 | #include "gfx/rect_io.h" |
29 | #include "os/system.h" |
30 | #include "os/window.h" |
31 | #include "ui/ui.h" |
32 | |
33 | #include <algorithm> |
34 | |
35 | namespace app { |
36 | |
37 | using namespace app::skin; |
38 | using namespace ui; |
39 | |
40 | static WidgetType colorbutton_type() |
41 | { |
42 | static WidgetType type = kGenericWidget; |
43 | if (type == kGenericWidget) |
44 | type = register_widget_type(); |
45 | return type; |
46 | } |
47 | |
48 | ColorButton::ColorButton(const app::Color& color, |
49 | const PixelFormat pixelFormat, |
50 | const ColorButtonOptions& options) |
51 | : ButtonBase("" , colorbutton_type(), kButtonWidget, kButtonWidget) |
52 | , m_color(color) |
53 | , m_pixelFormat(pixelFormat) |
54 | , m_window(nullptr) |
55 | , m_desktopCoords(false) |
56 | , m_dependOnLayer(false) |
57 | , m_options(options) |
58 | { |
59 | setFocusStop(true); |
60 | initTheme(); |
61 | |
62 | UIContext::instance()->add_observer(this); |
63 | } |
64 | |
65 | ColorButton::~ColorButton() |
66 | { |
67 | UIContext::instance()->remove_observer(this); |
68 | |
69 | delete m_window; // widget, window |
70 | } |
71 | |
72 | PixelFormat ColorButton::pixelFormat() const |
73 | { |
74 | return m_pixelFormat; |
75 | } |
76 | |
77 | void ColorButton::setPixelFormat(PixelFormat pixelFormat) |
78 | { |
79 | m_pixelFormat = pixelFormat; |
80 | invalidate(); |
81 | } |
82 | |
83 | app::Color ColorButton::getColor() const |
84 | { |
85 | return m_color; |
86 | } |
87 | |
88 | void ColorButton::setColor(const app::Color& origColor) |
89 | { |
90 | // Before change (this signal can modify the color) |
91 | app::Color color = origColor; |
92 | BeforeChange(color); |
93 | |
94 | m_color = color; |
95 | |
96 | // Change the color in its related window |
97 | if (m_window) { |
98 | // In the window we show the original color. In case |
99 | // BeforeChange() has changed the color type (e.g. to index), we |
100 | // don't care, in the window we prefer to keep the original |
101 | // HSV/HSL values. |
102 | m_window->setColor(origColor, ColorPopup::DontChangeType); |
103 | } |
104 | |
105 | // Emit signal |
106 | Change(color); |
107 | |
108 | invalidate(); |
109 | } |
110 | |
111 | app::Color ColorButton::getColorByPosition(const gfx::Point& pos) |
112 | { |
113 | // Ignore the position |
114 | return m_color; |
115 | } |
116 | |
117 | void ColorButton::onInitTheme(InitThemeEvent& ev) |
118 | { |
119 | ButtonBase::onInitTheme(ev); |
120 | |
121 | auto theme = SkinTheme::get(this); |
122 | setStyle(theme->styles.colorButton()); |
123 | |
124 | if (m_window) |
125 | m_window->initTheme(); |
126 | } |
127 | |
128 | bool ColorButton::onProcessMessage(Message* msg) |
129 | { |
130 | switch (msg->type()) { |
131 | |
132 | case kOpenMessage: |
133 | if (!m_windowDefaultBounds.isEmpty() && |
134 | this->isVisible()) { |
135 | openPopup(false); |
136 | } |
137 | break; |
138 | |
139 | case kCloseMessage: |
140 | if (m_window && m_window->isVisible()) |
141 | m_window->closeWindow(NULL); |
142 | break; |
143 | |
144 | case kMouseEnterMessage: |
145 | StatusBar::instance()->showColor(0, m_color); |
146 | break; |
147 | |
148 | case kMouseLeaveMessage: |
149 | StatusBar::instance()->showDefaultText(); |
150 | break; |
151 | |
152 | case kMouseMoveMessage: |
153 | // TODO code similar to TileButton::onProcessMessage() |
154 | if (hasCapture()) { |
155 | gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position(); |
156 | app::Color color = m_color; |
157 | |
158 | // Try to pick a color from a IColorSource, then get the color |
159 | // from the display surface, and finally from the desktop. The |
160 | // desktop must be a last resource method, because in macOS it |
161 | // will ask for permissions to record the screen. |
162 | os::Window* nativeWindow = msg->display()->nativeWindow(); |
163 | gfx::Point screenPos = nativeWindow->pointToScreen(mousePos); |
164 | |
165 | Widget* picked = manager()->pickFromScreenPos(screenPos); |
166 | if (picked == this) { |
167 | setColor(m_startDragColor); |
168 | break; |
169 | } |
170 | else { |
171 | m_mouseLeft = true; |
172 | } |
173 | |
174 | IColorSource* colorSource = dynamic_cast<IColorSource*>(picked); |
175 | if (colorSource) { |
176 | nativeWindow = picked->display()->nativeWindow(); |
177 | mousePos = nativeWindow->pointFromScreen(screenPos); |
178 | } |
179 | else { |
180 | gfx::Color gfxColor = gfx::ColorNone; |
181 | |
182 | // Get color from native window surface |
183 | if (nativeWindow->contentRect().contains(screenPos)) { |
184 | mousePos = nativeWindow->pointFromScreen(screenPos); |
185 | if (nativeWindow->surface()->bounds().contains(mousePos)) |
186 | gfxColor = nativeWindow->surface()->getPixel(mousePos.x, mousePos.y); |
187 | } |
188 | |
189 | // Or get the color from the screen |
190 | if (gfxColor == gfx::ColorNone) { |
191 | gfxColor = os::instance()->getColorFromScreen(screenPos); |
192 | } |
193 | |
194 | color = app::Color::fromRgb(gfx::getr(gfxColor), |
195 | gfx::getg(gfxColor), |
196 | gfx::getb(gfxColor), |
197 | gfx::geta(gfxColor)); |
198 | } |
199 | |
200 | if (colorSource) { |
201 | color = colorSource->getColorByPosition(mousePos); |
202 | } |
203 | |
204 | // Did the color change? |
205 | if (color != m_color) { |
206 | setColor(color); |
207 | } |
208 | } |
209 | break; |
210 | |
211 | case kSetCursorMessage: |
212 | if (hasCapture()) { |
213 | auto theme = SkinTheme::get(this); |
214 | ui::set_mouse_cursor(kCustomCursor, theme->cursors.eyedropper()); |
215 | return true; |
216 | } |
217 | break; |
218 | |
219 | } |
220 | |
221 | return ButtonBase::onProcessMessage(msg); |
222 | } |
223 | |
224 | void ColorButton::onSizeHint(SizeHintEvent& ev) |
225 | { |
226 | ButtonBase::onSizeHint(ev); |
227 | |
228 | gfx::Rect box; |
229 | getTextIconInfo(&box); |
230 | box.w = 64*guiscale(); |
231 | |
232 | gfx::Size sz = ev.sizeHint(); |
233 | sz.w = std::max(sz.w, box.w); |
234 | ev.setSizeHint(sz); |
235 | } |
236 | |
237 | void ColorButton::onPaint(PaintEvent& ev) |
238 | { |
239 | Graphics* g = ev.graphics(); |
240 | auto theme = SkinTheme::get(this); |
241 | gfx::Rect rc = clientBounds(); |
242 | |
243 | gfx::Color bg = bgColor(); |
244 | if (gfx::is_transparent(bg)) |
245 | bg = theme->colors.face(); |
246 | g->fillRect(bg, rc); |
247 | |
248 | app::Color color; |
249 | |
250 | // When the button is pushed, show the negative |
251 | m_dependOnLayer = false; |
252 | if (isSelected()) { |
253 | color = app::Color::fromRgb(255-m_color.getRed(), |
254 | 255-m_color.getGreen(), |
255 | 255-m_color.getBlue()); |
256 | } |
257 | // When the button is not pressed, show the real color |
258 | else { |
259 | color = m_color; |
260 | |
261 | // Show transparent color in indexed sprites as mask color when we |
262 | // are in a transparent layer. |
263 | if (color.getType() == app::Color::IndexType && |
264 | current_editor && |
265 | current_editor->sprite() && |
266 | current_editor->sprite()->pixelFormat() == IMAGE_INDEXED) { |
267 | m_dependOnLayer = true; |
268 | |
269 | if (int(current_editor->sprite()->transparentColor()) == color.getIndex() && |
270 | current_editor->layer() && |
271 | !current_editor->layer()->isBackground()) { |
272 | color = app::Color::fromMask(); |
273 | } |
274 | } |
275 | } |
276 | |
277 | draw_color_button(g, rc, |
278 | color, |
279 | (doc::ColorMode)m_pixelFormat, |
280 | hasMouseOver(), false); |
281 | |
282 | // Draw text |
283 | std::string str = m_color.toHumanReadableString(m_pixelFormat, |
284 | app::Color::ShortHumanReadableString); |
285 | |
286 | setTextQuiet(str.c_str()); |
287 | |
288 | gfx::Color textcolor = gfx::rgba(255, 255, 255); |
289 | if (color.isValid()) |
290 | textcolor = color_utils::blackandwhite_neg( |
291 | gfx::rgba(color.getRed(), color.getGreen(), color.getBlue())); |
292 | |
293 | gfx::Rect textrc; |
294 | getTextIconInfo(NULL, &textrc); |
295 | g->drawUIText(text(), textcolor, gfx::ColorNone, textrc.origin(), 0); |
296 | } |
297 | |
298 | void ColorButton::onClick(Event& ev) |
299 | { |
300 | ButtonBase::onClick(ev); |
301 | |
302 | // If the popup window was not created or shown yet.. |
303 | if (!m_window || !m_window->isVisible()) { |
304 | // Open it |
305 | openPopup(false); |
306 | } |
307 | else if (!m_window->isMoveable()) { |
308 | // If it is visible, close it |
309 | closePopup(); |
310 | } |
311 | } |
312 | |
313 | void ColorButton::onStartDrag() |
314 | { |
315 | m_startDragColor = m_color; |
316 | m_mouseLeft = false; |
317 | } |
318 | |
319 | void ColorButton::onSelectWhenDragging() |
320 | { |
321 | if (m_mouseLeft) |
322 | setSelected(false); |
323 | else |
324 | ButtonBase::onSelectWhenDragging(); |
325 | } |
326 | |
327 | void ColorButton::onLoadLayout(ui::LoadLayoutEvent& ev) |
328 | { |
329 | if (canPin()) { |
330 | bool pinned = false; |
331 | |
332 | m_desktopCoords = false; |
333 | ev.stream() >> pinned; |
334 | if (ev.stream() && pinned) |
335 | ev.stream() >> m_windowDefaultBounds |
336 | >> m_desktopCoords; |
337 | |
338 | m_hiddenPopupBounds = m_windowDefaultBounds; |
339 | } |
340 | } |
341 | |
342 | void ColorButton::onSaveLayout(ui::SaveLayoutEvent& ev) |
343 | { |
344 | if (canPin() && m_window && m_window->isPinned()) { |
345 | if (m_desktopCoords) |
346 | ev.stream() << 1 << ' ' << m_window->lastNativeFrame() << ' ' << 1; |
347 | else |
348 | ev.stream() << 1 << ' ' << m_window->bounds() << ' ' << 0; |
349 | } |
350 | else |
351 | ev.stream() << 0; |
352 | } |
353 | |
354 | bool ColorButton::() |
355 | { |
356 | return (m_window && m_window->isVisible()); |
357 | } |
358 | |
359 | void ColorButton::(const bool forcePinned) |
360 | { |
361 | const bool pinned = forcePinned || |
362 | (!m_windowDefaultBounds.isEmpty()); |
363 | |
364 | if (m_window == NULL) { |
365 | m_window = new ColorPopup(m_options); |
366 | m_window->Close.connect(&ColorButton::onWindowClose, this); |
367 | m_window->ColorChange.connect(&ColorButton::onWindowColorChange, this); |
368 | } |
369 | |
370 | m_window->setColor(m_color, ColorPopup::ChangeType); |
371 | m_window->remapWindow(); |
372 | |
373 | fit_bounds( |
374 | display(), |
375 | m_window, |
376 | gfx::Rect(m_window->sizeHint()), |
377 | [this, pinned, forcePinned](const gfx::Rect& workarea, |
378 | gfx::Rect& winBounds, |
379 | std::function<gfx::Rect(Widget*)> getWidgetBounds) { |
380 | if (!pinned || (forcePinned && m_hiddenPopupBounds.isEmpty())) { |
381 | gfx::Rect bounds = getWidgetBounds(this); |
382 | |
383 | winBounds.x = std::clamp(bounds.x, workarea.x, workarea.x2()-winBounds.w); |
384 | if (bounds.y2()+winBounds.h <= workarea.y2()) |
385 | winBounds.y = std::max(workarea.y, bounds.y2()); |
386 | else |
387 | winBounds.y = std::max(workarea.y, bounds.y-winBounds.h); |
388 | } |
389 | else if (forcePinned) { |
390 | winBounds = convertBounds(m_hiddenPopupBounds); |
391 | } |
392 | else { |
393 | winBounds = convertBounds(m_windowDefaultBounds); |
394 | } |
395 | }); |
396 | |
397 | m_window->openWindow(); |
398 | |
399 | m_window->setPinned(pinned); |
400 | |
401 | // Add the ColorButton area to the ColorPopup hot-region |
402 | if (!pinned) { |
403 | gfx::Rect rc = boundsOnScreen().createUnion(m_window->boundsOnScreen()); |
404 | rc.enlarge(8); |
405 | gfx::Region rgn(rc); |
406 | static_cast<PopupWindow*>(m_window)->setHotRegion(rgn); |
407 | } |
408 | |
409 | m_windowDefaultBounds = gfx::Rect(); |
410 | } |
411 | |
412 | void ColorButton::() |
413 | { |
414 | if (m_window) |
415 | m_window->closeWindow(nullptr); |
416 | } |
417 | |
418 | void ColorButton::onWindowClose(ui::CloseEvent& ev) |
419 | { |
420 | if (get_multiple_displays()) { |
421 | m_desktopCoords = true; |
422 | m_hiddenPopupBounds = m_window->lastNativeFrame(); |
423 | } |
424 | else { |
425 | m_desktopCoords = false; |
426 | m_hiddenPopupBounds = m_window->bounds(); |
427 | } |
428 | } |
429 | |
430 | void ColorButton::onWindowColorChange(const app::Color& color) |
431 | { |
432 | setColor(color); |
433 | } |
434 | |
435 | void ColorButton::onActiveSiteChange(const Site& site) |
436 | { |
437 | if (m_dependOnLayer) |
438 | invalidate(); |
439 | |
440 | if (canPin()) { |
441 | // Hide window |
442 | if (!site.document()) { |
443 | if (m_window) |
444 | m_window->setVisible(false); |
445 | } |
446 | // Show window if it's pinned |
447 | else { |
448 | // Check if it's pinned from the preferences (m_windowDefaultBounds) |
449 | if (!m_window && !m_windowDefaultBounds.isEmpty()) |
450 | openPopup(false); |
451 | // Or check if the window was hidden but it's pinned, so we've |
452 | // to show it again. |
453 | else if (m_window && m_window->isPinned()) |
454 | m_window->setVisible(true); |
455 | } |
456 | } |
457 | } |
458 | |
459 | gfx::Rect ColorButton::convertBounds(const gfx::Rect& bounds) const |
460 | { |
461 | // Convert to desktop |
462 | if (get_multiple_displays() && !m_desktopCoords) { |
463 | auto nativeWindow = display()->nativeWindow(); |
464 | return gfx::Rect(nativeWindow->pointToScreen(bounds.origin()), |
465 | nativeWindow->pointToScreen(bounds.point2())); |
466 | } |
467 | // Convert to display |
468 | else if (!get_multiple_displays() && m_desktopCoords) { |
469 | auto nativeWindow = display()->nativeWindow(); |
470 | return gfx::Rect(nativeWindow->pointFromScreen(bounds.origin()), |
471 | nativeWindow->pointFromScreen(bounds.point2())); |
472 | } |
473 | // No conversion is required |
474 | else |
475 | return bounds; |
476 | } |
477 | |
478 | } // namespace app |
479 | |