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
35namespace app {
36
37using namespace app::skin;
38using namespace ui;
39
40static WidgetType colorbutton_type()
41{
42 static WidgetType type = kGenericWidget;
43 if (type == kGenericWidget)
44 type = register_widget_type();
45 return type;
46}
47
48ColorButton::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
65ColorButton::~ColorButton()
66{
67 UIContext::instance()->remove_observer(this);
68
69 delete m_window; // widget, window
70}
71
72PixelFormat ColorButton::pixelFormat() const
73{
74 return m_pixelFormat;
75}
76
77void ColorButton::setPixelFormat(PixelFormat pixelFormat)
78{
79 m_pixelFormat = pixelFormat;
80 invalidate();
81}
82
83app::Color ColorButton::getColor() const
84{
85 return m_color;
86}
87
88void 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
111app::Color ColorButton::getColorByPosition(const gfx::Point& pos)
112{
113 // Ignore the position
114 return m_color;
115}
116
117void 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
128bool 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
224void 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
237void 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
298void 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
313void ColorButton::onStartDrag()
314{
315 m_startDragColor = m_color;
316 m_mouseLeft = false;
317}
318
319void ColorButton::onSelectWhenDragging()
320{
321 if (m_mouseLeft)
322 setSelected(false);
323 else
324 ButtonBase::onSelectWhenDragging();
325}
326
327void 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
342void 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
354bool ColorButton::isPopupVisible()
355{
356 return (m_window && m_window->isVisible());
357}
358
359void ColorButton::openPopup(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
412void ColorButton::closePopup()
413{
414 if (m_window)
415 m_window->closeWindow(nullptr);
416}
417
418void 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
430void ColorButton::onWindowColorChange(const app::Color& color)
431{
432 setColor(color);
433}
434
435void 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
459gfx::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