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_popup.h"
13
14#include "app/app.h"
15#include "app/cmd/set_palette.h"
16#include "app/color.h"
17#include "app/console.h"
18#include "app/context.h"
19#include "app/context_access.h"
20#include "app/doc.h"
21#include "app/file/palette_file.h"
22#include "app/i18n/strings.h"
23#include "app/modules/gfx.h"
24#include "app/modules/gui.h"
25#include "app/modules/palettes.h"
26#include "app/resource_finder.h"
27#include "app/transaction.h"
28#include "app/ui/color_bar.h"
29#include "app/ui/palette_view.h"
30#include "app/ui/skin/skin_theme.h"
31#include "app/ui_context.h"
32#include "base/scoped_value.h"
33#include "doc/image_impl.h"
34#include "doc/palette.h"
35#include "doc/sprite.h"
36#include "gfx/border.h"
37#include "gfx/size.h"
38#include "ui/ui.h"
39
40namespace app {
41
42using namespace ui;
43using namespace doc;
44
45enum {
46 INDEX_MODE,
47 RGB_MODE,
48 HSV_MODE,
49 HSL_MODE,
50 GRAY_MODE,
51 MASK_MODE,
52 COLOR_MODES
53};
54
55static std::unique_ptr<doc::Palette> g_simplePal(nullptr);
56
57class ColorPopup::SimpleColors : public HBox {
58public:
59
60 class Item : public Button {
61 public:
62 Item(ColorPopup* colorPopup, const app::Color& color)
63 : Button("")
64 , m_colorPopup(colorPopup)
65 , m_color(color) {
66 }
67
68 private:
69 void onClick(Event& ev) override {
70 m_colorPopup->setColorWithSignal(m_color, ChangeType);
71 }
72
73 void onPaint(PaintEvent& ev) override {
74 Graphics* g = ev.graphics();
75 auto theme = skin::SkinTheme::get(this);
76 gfx::Rect rc = clientBounds();
77
78 Button::onPaint(ev);
79
80 rc.shrink(theme->calcBorder(this, style()));
81 draw_color(g, rc, m_color, doc::ColorMode::RGB);
82 }
83
84 ColorPopup* m_colorPopup;
85 app::Color m_color;
86 };
87
88 SimpleColors(ColorPopup* colorPopup, TooltipManager* tooltips) {
89 for (int i=0; i<g_simplePal->size(); ++i) {
90 doc::color_t c = g_simplePal->getEntry(i);
91 app::Color color =
92 app::Color::fromRgb(doc::rgba_getr(c),
93 doc::rgba_getg(c),
94 doc::rgba_getb(c),
95 doc::rgba_geta(c));
96
97 Item* item = new Item(colorPopup, color);
98 item->InitTheme.connect(
99 [item]{
100 auto theme = skin::SkinTheme::get(item);
101 item->setSizeHint(gfx::Size(16, 16)*ui::guiscale());
102 item->setStyle(theme->styles.simpleColor());
103 });
104 item->initTheme();
105 addChild(item);
106
107 tooltips->addTooltipFor(
108 item, g_simplePal->getEntryName(i), BOTTOM);
109 }
110 }
111
112 void selectColor(int index) {
113 for (int i=0; i<g_simplePal->size(); ++i) {
114 children()[i]->setSelected(i == index);
115 }
116 }
117
118 void deselect() {
119 for (int i=0; i<g_simplePal->size(); ++i) {
120 children()[i]->setSelected(false);
121 }
122 }
123};
124
125ColorPopup::CustomButtonSet::CustomButtonSet()
126 : ButtonSet(COLOR_MODES)
127{
128}
129
130void ColorPopup::CustomButtonSet::onSelectItem(Item* item, bool focusItem, ui::Message* msg)
131{
132 int count = countSelectedItems();
133 int itemIndex = getItemIndex(item);
134
135 if (itemIndex == INDEX_MODE ||
136 itemIndex == MASK_MODE ||
137 !msg ||
138 // Any key modifier will act as multiple selection
139 (!msg->shiftPressed() &&
140 !msg->altPressed() &&
141 !msg->ctrlPressed() &&
142 !msg->cmdPressed())) {
143 if (item &&
144 item->isSelected() &&
145 count == 1)
146 return;
147
148 for (int i=0; i<COLOR_MODES; ++i)
149 if (getItem(i)->isSelected())
150 getItem(i)->setSelected(false);
151 }
152 else {
153 if (getItem(INDEX_MODE)->isSelected()) getItem(INDEX_MODE)->setSelected(false);
154 if (getItem(MASK_MODE)->isSelected()) getItem(MASK_MODE)->setSelected(false);
155 }
156
157 if (item) {
158 // Item already selected
159 if (count == 1 && item == findSelectedItem())
160 return;
161
162 item->setSelected(!item->isSelected());
163 if (focusItem)
164 item->requestFocus();
165 }
166}
167
168ColorPopup::ColorPopup(const ColorButtonOptions& options)
169 : PopupWindowPin(" ", // Non-empty to create title-bar and close button
170 ClickBehavior::CloseOnClickInOtherWindow,
171 options.canPinSelector)
172 , m_vbox(VERTICAL)
173 , m_topBox(HORIZONTAL)
174 , m_color(app::Color::fromMask())
175 , m_closeButton(nullptr)
176 , m_colorPaletteContainer(options.showIndexTab ?
177 new ui::View: nullptr)
178 , m_colorPalette(options.showIndexTab ?
179 new PaletteView(false, PaletteView::SelectOneColor, this, 7*guiscale()):
180 nullptr)
181 , m_simpleColors(nullptr)
182 , m_oldAndNew(Shade(2), ColorShades::ClickEntries)
183 , m_maskLabel(Strings::color_popup_transparent_color_sel())
184 , m_canPin(options.canPinSelector)
185 , m_insideChange(false)
186 , m_disableHexUpdate(false)
187{
188 if (options.showSimpleColors) {
189 if (!g_simplePal) {
190 ResourceFinder rf;
191 rf.includeDataDir("palettes/tags.gpl");
192 if (rf.findFirst())
193 g_simplePal = load_palette(rf.filename().c_str());
194 }
195
196 if (g_simplePal)
197 m_simpleColors = new SimpleColors(this, &m_tooltips);
198 }
199
200 ButtonSet::Item* item = m_colorType.addItem(Strings::color_popup_index());
201 item->setFocusStop(false);
202 if (!options.showIndexTab)
203 item->setVisible(false);
204
205 m_colorType.addItem("RGB")->setFocusStop(false);
206 m_colorType.addItem("HSV")->setFocusStop(false);
207 m_colorType.addItem("HSL")->setFocusStop(false);
208 m_colorType.addItem("Gray")->setFocusStop(false);
209 m_colorType.addItem("Mask")->setFocusStop(false);
210
211 m_topBox.setBorder(gfx::Border(0));
212 m_topBox.setChildSpacing(0);
213
214 if (m_colorPalette) {
215 m_colorPaletteContainer->attachToView(m_colorPalette);
216 m_colorPaletteContainer->setExpansive(true);
217 }
218 m_sliders.setExpansive(true);
219
220 m_topBox.addChild(&m_colorType);
221 m_topBox.addChild(new Separator("", VERTICAL));
222 m_topBox.addChild(&m_hexColorEntry);
223 m_topBox.addChild(&m_oldAndNew);
224
225 // TODO fix this hack for close button in popup window
226 // Move close button (decorative widget) inside the m_topBox
227 {
228 WidgetsList decorators;
229 for (auto child : children()) {
230 if (child->type() == kWindowCloseButtonWidget) {
231 m_closeButton = child;
232 removeChild(child);
233 break;
234 }
235 }
236 if (m_closeButton) {
237 m_topBox.addChild(new BoxFiller);
238 VBox* vbox = new VBox;
239 vbox->addChild(m_closeButton);
240 m_topBox.addChild(vbox);
241 }
242 }
243 setText(""); // To remove title
244
245 m_vbox.addChild(&m_tooltips);
246 if (m_simpleColors)
247 m_vbox.addChild(m_simpleColors);
248 m_vbox.addChild(&m_topBox);
249 if (m_colorPaletteContainer)
250 m_vbox.addChild(m_colorPaletteContainer);
251 m_vbox.addChild(&m_sliders);
252 m_vbox.addChild(&m_maskLabel);
253 addChild(&m_vbox);
254
255 m_colorType.ItemChange.connect([this]{ onColorTypeClick(); });
256
257 m_sliders.ColorChange.connect(&ColorPopup::onColorSlidersChange, this);
258 m_hexColorEntry.ColorChange.connect(&ColorPopup::onColorHexEntryChange, this);
259 m_oldAndNew.Click.connect(&ColorPopup::onSelectOldColor, this);
260
261 // Set RGB just for the sizeHint(), and then deselect the color type
262 // (the first setColor() call will setup it correctly.)
263 selectColorType(app::Color::RgbType);
264 m_colorType.deselectItems();
265
266 m_onPaletteChangeConn =
267 App::instance()->PaletteChange.connect(&ColorPopup::onPaletteChange, this);
268
269 InitTheme.connect(
270 [this]{
271 setSizeHint(gfx::Size(300*guiscale(), sizeHint().h));
272 });
273 initTheme();
274}
275
276ColorPopup::~ColorPopup()
277{
278}
279
280void ColorPopup::setColor(const app::Color& color,
281 const SetColorOptions options)
282{
283 m_color = color;
284
285 if (m_simpleColors) {
286 int r = color.getRed();
287 int g = color.getGreen();
288 int b = color.getBlue();
289 int a = color.getAlpha();
290 int i = g_simplePal->findExactMatch(r, g, b, a, -1);
291 if (i >= 0)
292 m_simpleColors->selectColor(i);
293 else
294 m_simpleColors->deselect();
295 }
296
297 if (color.getType() == app::Color::IndexType) {
298 if (m_colorPalette) {
299 m_colorPalette->deselect();
300 m_colorPalette->selectColor(color.getIndex());
301 }
302 }
303
304 m_sliders.setColor(m_color);
305 if (!m_disableHexUpdate)
306 m_hexColorEntry.setColor(m_color);
307
308 if (options == ChangeType)
309 selectColorType(m_color.getType());
310
311 // Set the new color
312 Shade shade = m_oldAndNew.getShade();
313 shade.resize(2);
314 shade[1] = (color.getType() == app::Color::IndexType ? color.toRgb(): color);
315 if (!m_insideChange)
316 shade[0] = shade[1];
317 m_oldAndNew.setShade(shade);
318}
319
320app::Color ColorPopup::getColor() const
321{
322 return m_color;
323}
324
325bool ColorPopup::onProcessMessage(ui::Message* msg)
326{
327 switch (msg->type()) {
328 case kSetCursorMessage: {
329 if (m_canPin) {
330 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
331 if (hitTest(mousePos) == HitTestCaption) {
332 set_mouse_cursor(kMoveCursor);
333 return true;
334 }
335 }
336 break;
337 }
338 }
339 return PopupWindowPin::onProcessMessage(msg);
340}
341
342void ColorPopup::onWindowResize()
343{
344 PopupWindowPin::onWindowResize();
345
346 if (m_closeButton) {
347 gfx::Rect rc = m_closeButton->bounds();
348 if (rc.x2() > bounds().x2()) {
349 rc.x = bounds().x2() - rc.w;
350 m_closeButton->setBounds(rc);
351 }
352 }
353}
354
355void ColorPopup::onMakeFloating()
356{
357 PopupWindowPin::onMakeFloating();
358
359 if (m_canPin) {
360 setSizeable(true);
361 setMoveable(true);
362 }
363}
364
365void ColorPopup::onMakeFixed()
366{
367 PopupWindowPin::onMakeFixed();
368
369 if (m_canPin) {
370 setSizeable(false);
371 setMoveable(true);
372 }
373}
374
375void ColorPopup::onPaletteViewIndexChange(int index, ui::MouseButton button)
376{
377 base::ScopedValue<bool> restore(m_insideChange, true,
378 m_insideChange);
379
380 setColorWithSignal(app::Color::fromIndex(index), ChangeType);
381}
382
383void ColorPopup::onColorSlidersChange(ColorSlidersChangeEvent& ev)
384{
385 base::ScopedValue<bool> restore(m_insideChange, true,
386 m_insideChange);
387
388 setColorWithSignal(ev.color(), DontChangeType);
389 findBestfitIndex(ev.color());
390}
391
392void ColorPopup::onColorHexEntryChange(const app::Color& color)
393{
394 base::ScopedValue<bool> restore(m_insideChange, true,
395 m_insideChange);
396
397 // Disable updating the hex entry so we don't override what the user
398 // is writting in the text field.
399 m_disableHexUpdate = true;
400
401 setColorWithSignal(color, ChangeType);
402 findBestfitIndex(color);
403
404 // If we are in edit mode, the "m_disableHexUpdate" will be changed
405 // to false in onPaletteChange() after the color bar timer is
406 // triggered. In this way we don't modify the hex field when the
407 // user is editing it and the palette "edit mode" is enabled.
408 if (!inEditMode())
409 m_disableHexUpdate = false;
410}
411
412void ColorPopup::onSelectOldColor(ColorShades::ClickEvent&)
413{
414 Shade shade = m_oldAndNew.getShade();
415 int hot = m_oldAndNew.getHotEntry();
416 if (hot >= 0 &&
417 hot < int(shade.size())) {
418 setColorWithSignal(shade[hot], DontChangeType);
419 }
420}
421
422void ColorPopup::onSimpleColorClick()
423{
424 m_colorType.deselectItems();
425 if (!g_simplePal)
426 return;
427
428 app::Color color = getColor();
429
430 // Find bestfit palette entry
431 int r = color.getRed();
432 int g = color.getGreen();
433 int b = color.getBlue();
434 int a = color.getAlpha();
435
436 // Search for the closest color to the RGB values
437 int i = g_simplePal->findBestfit(r, g, b, a, 0);
438 if (i >= 0) {
439 color_t c = g_simplePal->getEntry(i);
440 color = app::Color::fromRgb(doc::rgba_getr(c),
441 doc::rgba_getg(c),
442 doc::rgba_getb(c),
443 doc::rgba_geta(c));
444 }
445
446 setColorWithSignal(color, ChangeType);
447}
448
449void ColorPopup::onColorTypeClick()
450{
451 base::ScopedValue<bool> restore(m_insideChange, true,
452 m_insideChange);
453
454 if (m_simpleColors)
455 m_simpleColors->deselect();
456
457 app::Color newColor = getColor();
458
459 switch (m_colorType.selectedItem()) {
460 case INDEX_MODE:
461 newColor = app::Color::fromIndex(newColor.getIndex());
462 break;
463 case RGB_MODE:
464 newColor = app::Color::fromRgb(newColor.getRed(),
465 newColor.getGreen(),
466 newColor.getBlue(),
467 newColor.getAlpha());
468 break;
469 case HSV_MODE:
470 newColor = app::Color::fromHsv(newColor.getHsvHue(),
471 newColor.getHsvSaturation(),
472 newColor.getHsvValue(),
473 newColor.getAlpha());
474 break;
475 case HSL_MODE:
476 newColor = app::Color::fromHsl(newColor.getHslHue(),
477 newColor.getHslSaturation(),
478 newColor.getHslLightness(),
479 newColor.getAlpha());
480 break;
481 case GRAY_MODE:
482 newColor = app::Color::fromGray(newColor.getGray(),
483 newColor.getAlpha());
484 break;
485 case MASK_MODE:
486 newColor = app::Color::fromMask();
487 break;
488 }
489
490 setColorWithSignal(newColor, ChangeType);
491}
492
493void ColorPopup::onPaletteChange()
494{
495 base::ScopedValue<bool> restore(m_insideChange, inEditMode(),
496 m_insideChange);
497
498 setColor(getColor(), DontChangeType);
499 invalidate();
500
501 if (inEditMode())
502 m_disableHexUpdate = false;
503}
504
505void ColorPopup::findBestfitIndex(const app::Color& color)
506{
507 if (!m_colorPalette)
508 return;
509
510 // Find bestfit palette entry
511 int r = color.getRed();
512 int g = color.getGreen();
513 int b = color.getBlue();
514 int a = color.getAlpha();
515
516 // Search for the closest color to the RGB values
517 int i = get_current_palette()->findBestfit(r, g, b, a, 0);
518 if (i >= 0) {
519 m_colorPalette->deselect();
520 m_colorPalette->selectColor(i);
521 }
522}
523
524void ColorPopup::setColorWithSignal(const app::Color& color,
525 const SetColorOptions options)
526{
527 Shade shade = m_oldAndNew.getShade();
528
529 setColor(color, options);
530
531 shade.resize(2);
532 shade[1] = color;
533 m_oldAndNew.setShade(shade);
534
535 // Fire ColorChange signal
536 ColorChange(color);
537}
538
539void ColorPopup::selectColorType(app::Color::Type type)
540{
541 if (m_colorPaletteContainer)
542 m_colorPaletteContainer->setVisible(type == app::Color::IndexType);
543
544 m_maskLabel.setVisible(type == app::Color::MaskType);
545
546 // Count selected items.
547 if (m_colorType.countSelectedItems() < 2) {
548 switch (type) {
549 case app::Color::IndexType:
550 if (m_colorPalette)
551 m_colorType.setSelectedItem(INDEX_MODE);
552 else
553 m_colorType.setSelectedItem(RGB_MODE);
554 break;
555 case app::Color::RgbType: m_colorType.setSelectedItem(RGB_MODE); break;
556 case app::Color::HsvType: m_colorType.setSelectedItem(HSV_MODE); break;
557 case app::Color::HslType: m_colorType.setSelectedItem(HSL_MODE); break;
558 case app::Color::GrayType: m_colorType.setSelectedItem(GRAY_MODE); break;
559 case app::Color::MaskType: m_colorType.setSelectedItem(MASK_MODE); break;
560 }
561 }
562
563 std::vector<app::Color::Type> types;
564 if (m_colorType.getItem(RGB_MODE)->isSelected())
565 types.push_back(app::Color::RgbType);
566 if (m_colorType.getItem(HSV_MODE)->isSelected())
567 types.push_back(app::Color::HsvType);
568 if (m_colorType.getItem(HSL_MODE)->isSelected())
569 types.push_back(app::Color::HslType);
570 if (m_colorType.getItem(GRAY_MODE)->isSelected())
571 types.push_back(app::Color::GrayType);
572 m_sliders.setColorTypes(types);
573
574 // Remove focus from hidden RGB/HSV/HSL text entries
575 auto widget = manager()->getFocus();
576 if (widget && !widget->isVisible()) {
577 auto window = widget->window();
578 if (window && window == this)
579 widget->releaseFocus();
580 }
581
582 m_vbox.layout();
583 m_vbox.invalidate();
584}
585
586bool ColorPopup::inEditMode()
587{
588 return
589 // TODO use other flag instead of m_canPin, here we want to ask if
590 // this ColorPopup is related to the main ColorBar (instead of
591 // other ColorButtons like the one in "Replace Color", etc.)
592 (m_canPin) &&
593 (ColorBar::instance()->inEditMode());
594}
595
596} // namespace app
597