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 | |
40 | namespace app { |
41 | |
42 | using namespace ui; |
43 | using namespace doc; |
44 | |
45 | enum { |
46 | INDEX_MODE, |
47 | RGB_MODE, |
48 | HSV_MODE, |
49 | HSL_MODE, |
50 | GRAY_MODE, |
51 | MASK_MODE, |
52 | COLOR_MODES |
53 | }; |
54 | |
55 | static std::unique_ptr<doc::Palette> g_simplePal(nullptr); |
56 | |
57 | class ColorPopup:: : public HBox { |
58 | public: |
59 | |
60 | class : public Button { |
61 | public: |
62 | (ColorPopup* , const app::Color& color) |
63 | : Button("" ) |
64 | , m_colorPopup(colorPopup) |
65 | , m_color(color) { |
66 | } |
67 | |
68 | private: |
69 | void (Event& ev) override { |
70 | m_colorPopup->setColorWithSignal(m_color, ChangeType); |
71 | } |
72 | |
73 | void (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* ; |
85 | app::Color ; |
86 | }; |
87 | |
88 | (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 (int index) { |
113 | for (int i=0; i<g_simplePal->size(); ++i) { |
114 | children()[i]->setSelected(i == index); |
115 | } |
116 | } |
117 | |
118 | void () { |
119 | for (int i=0; i<g_simplePal->size(); ++i) { |
120 | children()[i]->setSelected(false); |
121 | } |
122 | } |
123 | }; |
124 | |
125 | ColorPopup::CustomButtonSet::() |
126 | : ButtonSet(COLOR_MODES) |
127 | { |
128 | } |
129 | |
130 | void ColorPopup::CustomButtonSet::(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 | |
168 | 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 | |
276 | ColorPopup::() |
277 | { |
278 | } |
279 | |
280 | void ColorPopup::(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 | |
320 | app::Color ColorPopup::() const |
321 | { |
322 | return m_color; |
323 | } |
324 | |
325 | bool ColorPopup::(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 | |
342 | void ColorPopup::() |
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 | |
355 | void ColorPopup::() |
356 | { |
357 | PopupWindowPin::onMakeFloating(); |
358 | |
359 | if (m_canPin) { |
360 | setSizeable(true); |
361 | setMoveable(true); |
362 | } |
363 | } |
364 | |
365 | void ColorPopup::() |
366 | { |
367 | PopupWindowPin::onMakeFixed(); |
368 | |
369 | if (m_canPin) { |
370 | setSizeable(false); |
371 | setMoveable(true); |
372 | } |
373 | } |
374 | |
375 | void ColorPopup::(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 | |
383 | void ColorPopup::(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 | |
392 | void ColorPopup::(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 | |
412 | void ColorPopup::(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 | |
422 | void ColorPopup::() |
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 | |
449 | void ColorPopup::() |
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 | |
493 | void ColorPopup::() |
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 | |
505 | void ColorPopup::(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 | |
524 | void ColorPopup::(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 | |
539 | void ColorPopup::(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 | |
586 | bool ColorPopup::() |
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 | |