1// Aseprite
2// Copyright (C) 2018-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#define COLOR_BAR_TRACE(...) // TRACE(__VA_ARGS__)
9
10#ifdef HAVE_CONFIG_H
11#include "config.h"
12#endif
13
14#include "app/ui/color_bar.h"
15
16#include "app/app.h"
17#include "app/app_menus.h"
18#include "app/cmd/remap_colors.h"
19#include "app/cmd/remap_tilemaps.h"
20#include "app/cmd/remap_tileset.h"
21#include "app/cmd/remove_tile.h"
22#include "app/cmd/replace_image.h"
23#include "app/cmd/set_palette.h"
24#include "app/cmd/set_transparent_color.h"
25#include "app/cmd_sequence.h"
26#include "app/color.h"
27#include "app/commands/command.h"
28#include "app/commands/commands.h"
29#include "app/commands/params.h"
30#include "app/commands/quick_command.h"
31#include "app/console.h"
32#include "app/context_access.h"
33#include "app/doc_api.h"
34#include "app/doc_undo.h"
35#include "app/i18n/strings.h"
36#include "app/ini_file.h"
37#include "app/inline_command_execution.h"
38#include "app/modules/editors.h"
39#include "app/modules/gui.h"
40#include "app/modules/palettes.h"
41#include "app/pref/preferences.h"
42#include "app/tx.h"
43#include "app/ui/color_spectrum.h"
44#include "app/ui/color_tint_shade_tone.h"
45#include "app/ui/color_wheel.h"
46#include "app/ui/editor/editor.h"
47#include "app/ui/hex_color_entry.h"
48#include "app/ui/input_chain.h"
49#include "app/ui/keyboard_shortcuts.h"
50#include "app/ui/palette_popup.h"
51#include "app/ui/skin/skin_theme.h"
52#include "app/ui/status_bar.h"
53#include "app/ui/timeline/timeline.h"
54#include "app/ui_context.h"
55#include "app/ui_context.h"
56#include "app/util/cel_ops.h"
57#include "app/util/clipboard.h"
58#include "base/scoped_value.h"
59#include "doc/cel.h"
60#include "doc/cels_range.h"
61#include "doc/image.h"
62#include "doc/image_impl.h"
63#include "doc/layer_tilemap.h"
64#include "doc/palette.h"
65#include "doc/palette_gradient_type.h"
66#include "doc/primitives.h"
67#include "doc/remap.h"
68#include "doc/rgbmap.h"
69#include "doc/sort_palette.h"
70#include "doc/sprite.h"
71#include "doc/tileset.h"
72#include "os/surface.h"
73#include "ui/alert.h"
74#include "ui/graphics.h"
75#include "ui/menu.h"
76#include "ui/message.h"
77#include "ui/paint_event.h"
78#include "ui/separator.h"
79#include "ui/splitter.h"
80#include "ui/system.h"
81#include "ui/tooltips.h"
82
83#include <cstring>
84#include <limits>
85#include <memory>
86
87namespace app {
88
89enum class PalButton {
90 SORT,
91 PRESETS,
92 OPTIONS,
93 MAX
94};
95
96using namespace app::skin;
97using namespace ui;
98
99class ColorBar::WarningIcon : public ui::Button {
100public:
101 WarningIcon() : ui::Button(std::string()) {
102 initTheme();
103 }
104protected:
105 void onInitTheme(ui::InitThemeEvent& ev) {
106 ui::Button::onInitTheme(ev);
107
108 auto theme = skin::SkinTheme::get(this);
109 setStyle(theme->styles.warningBox());
110 }
111};
112
113//////////////////////////////////////////////////////////////////////
114// ColorBar::ScrollableView class
115
116ColorBar::ScrollableView::ScrollableView()
117{
118 initTheme();
119}
120
121void ColorBar::ScrollableView::onInitTheme(InitThemeEvent& ev)
122{
123 auto hbar = horizontalBar();
124 auto vbar = verticalBar();
125 setup_mini_look(hbar);
126 setup_mini_look(vbar);
127
128 View::onInitTheme(ev);
129
130 auto theme = SkinTheme::get(this);
131
132 setStyle(theme->styles.colorbarView());
133
134 hbar->setStyle(theme->styles.miniScrollbar());
135 vbar->setStyle(theme->styles.miniScrollbar());
136 hbar->setThumbStyle(theme->styles.miniScrollbarThumb());
137 vbar->setThumbStyle(theme->styles.miniScrollbarThumb());
138
139 const int scrollBarWidth = theme->dimensions.miniScrollbarSize();
140 hbar->setBarWidth(scrollBarWidth);
141 vbar->setBarWidth(scrollBarWidth);
142}
143
144//////////////////////////////////////////////////////////////////////
145// ColorBar class
146
147ColorBar* ColorBar::m_instance = NULL;
148
149ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
150 : Box(align)
151 , m_editPal(1)
152 , m_buttons(int(PalButton::MAX))
153 , m_tilesButton(1)
154 , m_tilesetModeButtons(3)
155 , m_splitter(Splitter::ByPercentage, VERTICAL)
156 , m_splitterPalTil(Splitter::ByPercentage, VERTICAL)
157 , m_paletteView(true, PaletteView::FgBgColors, this, 16)
158 , m_tilesView(true, PaletteView::FgBgTiles, this, 16)
159 , m_remapPalButton(Strings::color_bar_remap_palette())
160 , m_remapTilesButton(Strings::color_bar_remap_tiles())
161 , m_selector(ColorSelector::NONE)
162 , m_tintShadeTone(nullptr)
163 , m_spectrum(nullptr)
164 , m_wheel(nullptr)
165 , m_fgColor(app::Color::fromRgb(255, 255, 255), IMAGE_RGB, ColorBarButtonsOptions())
166 , m_bgColor(app::Color::fromRgb(0, 0, 0), IMAGE_RGB, ColorBarButtonsOptions())
167 , m_fgWarningIcon(new WarningIcon)
168 , m_bgWarningIcon(new WarningIcon)
169 , m_fromPalView(false)
170 , m_fromPref(false)
171 , m_fromFgButton(false)
172 , m_fromBgButton(false)
173 , m_lastDocument(nullptr)
174 , m_lastTilesetId(doc::NullId)
175 , m_ascending(true)
176 , m_lastButton(kButtonLeft)
177 , m_editMode(false)
178 , m_tilemapMode(TilemapMode::Pixels)
179 , m_tilesetMode(TilesetMode::Auto)
180 , m_redrawTimer(250, this)
181 , m_redrawAll(false)
182 , m_implantChange(false)
183 , m_selfPalChange(false)
184 , m_splitterPalTilPos(50.0)
185{
186 m_instance = this;
187
188 auto theme = SkinTheme::get(this);
189
190 m_editPal.addItem(theme->parts.timelineOpenPadlockActive());
191 m_buttons.addItem(theme->parts.palSort());
192 m_buttons.addItem(theme->parts.palPresets());
193 m_buttons.addItem(theme->parts.palOptions());
194 m_tilesButton.addItem(theme->parts.tiles());
195
196 static_assert(0 == int(TilesetMode::Manual) &&
197 1 == int(TilesetMode::Auto) &&
198 2 == int(TilesetMode::Stack), "Tileset mode buttons doesn't match TilesetMode enum values");
199
200 m_tilesetModeButtons.addItem(theme->parts.tilesManual());
201 m_tilesetModeButtons.addItem(theme->parts.tilesAuto());
202 m_tilesetModeButtons.addItem(theme->parts.tilesStack());
203 setTilesetMode(m_tilesetMode);
204
205 m_paletteView.setColumns(8);
206 m_tilesView.setColumns(8);
207
208 m_scrollablePalView.attachToView(&m_paletteView);
209 m_scrollableTilesView.attachToView(&m_tilesView);
210 m_scrollablePalView.setExpansive(true);
211 m_scrollableTilesView.setExpansive(true);
212 m_splitterPalTil.setExpansive(true);
213
214 m_scrollableTilesView.setVisible(false);
215 m_tilesHelpers.setVisible(false);
216 m_remapPalButton.setVisible(false);
217 m_remapTilesButton.setVisible(false);
218
219 m_splitterPalTil.addChild(&m_scrollablePalView);
220 m_splitterPalTil.addChild(&m_scrollableTilesView);
221 m_palettePlaceholder.addChild(&m_splitterPalTil);
222 m_palettePlaceholder.addChild(&m_remapPalButton);
223 m_palettePlaceholder.addChild(&m_remapTilesButton);
224 m_splitter.setId("palette_spectrum_splitter");
225 m_splitter.setPosition(80);
226 m_splitter.setExpansive(true);
227 m_splitter.addChild(&m_palettePlaceholder);
228 m_splitter.addChild(&m_selectorPlaceholder);
229
230 setColorSelector(
231 Preferences::instance().colorBar.selector());
232
233 m_tilesHBox.addChild(&m_tilesButton);
234 m_tilesHBox.addChild(&m_tilesetModeButtons);
235
236 m_palHBox.addChild(&m_editPal);
237 m_palHBox.addChild(&m_buttons);
238
239 m_buttons.setExpansive(true);
240 m_tilesetModeButtons.setExpansive(true);
241
242 // Hide the tiles controls by default. Without this, when the first
243 // onActiveSiteChange() event is received, and the we ask for the
244 // m_tilesHBox visibility, it might say that it's hidden because the
245 // color bar is hidden (because it's not yet in the screen, it's the
246 // first time it will be displayed). So we have to add this to make
247 // the tiles controls invisible in the first appearance of the color
248 // bar.
249 m_tilesHBox.setVisible(false);
250
251 addChild(&m_palHBox);
252 addChild(&m_tilesHBox);
253 addChild(&m_splitter);
254
255 addChild(&m_colorHelpers);
256 addChild(&m_tilesHelpers);
257
258 HBox* fgBox = new HBox;
259 HBox* bgBox = new HBox;
260 fgBox->addChild(&m_fgColor);
261 fgBox->addChild(m_fgWarningIcon);
262 bgBox->addChild(&m_bgColor);
263 bgBox->addChild(m_bgWarningIcon);
264 m_colorHelpers.addChild(fgBox);
265 m_colorHelpers.addChild(bgBox);
266
267 m_tilesHelpers.addChild(new BoxFiller);
268 m_tilesHelpers.addChild(&m_fgTile);
269 m_tilesHelpers.addChild(&m_bgTile);
270 m_tilesHelpers.addChild(new BoxFiller);
271
272 m_fgColor.setId("fg_color");
273 m_bgColor.setId("bg_color");
274 m_fgColor.setExpansive(true);
275 m_bgColor.setExpansive(true);
276
277 m_remapPalButton.Click.connect([this]{ onRemapPalButtonClick(); });
278 m_remapTilesButton.Click.connect([this]{ onRemapTilesButtonClick(); });
279 m_fgColor.Change.connect(&ColorBar::onFgColorButtonChange, this);
280 m_fgColor.BeforeChange.connect(&ColorBar::onFgColorButtonBeforeChange, this);
281 m_bgColor.Change.connect(&ColorBar::onBgColorButtonChange, this);
282 m_fgWarningIcon->Click.connect([this]{ onFixWarningClick(&m_fgColor, m_fgWarningIcon); });
283 m_bgWarningIcon->Click.connect([this]{ onFixWarningClick(&m_bgColor, m_bgWarningIcon); });
284 m_redrawTimer.Tick.connect([this]{ onTimerTick(); });
285 m_editPal.ItemChange.connect([this]{ onSwitchPalEditMode(); });
286 m_buttons.ItemChange.connect([this]{ onPaletteButtonClick(); });
287 m_tilesButton.ItemChange.connect([this]{ onTilesButtonClick(); });
288 m_tilesButton.RightClick.connect([this]{ onTilesButtonRightClick(); });
289 m_fgTile.Change.connect(&ColorBar::onFgTileButtonChange, this);
290 m_bgTile.Change.connect(&ColorBar::onBgTileButtonChange, this);
291 m_tilesetModeButtons.ItemChange.connect([this]{ onTilesetModeButtonClick(); });
292
293 InitTheme.connect(
294 [this, fgBox, bgBox]{
295 auto theme = SkinTheme::get(this);
296
297 setBorder(gfx::Border(2*guiscale(), 0, 0, 0));
298 setChildSpacing(2*guiscale());
299
300 m_fgColor.resetSizeHint();
301 m_bgColor.resetSizeHint();
302 m_fgColor.setSizeHint(0, m_fgColor.sizeHint().h);
303 m_bgColor.setSizeHint(0, m_bgColor.sizeHint().h);
304
305 for (auto w : { &m_editPal, &m_buttons,
306 &m_tilesButton, &m_tilesetModeButtons }) {
307 w->setMinSize(gfx::Size(0, theme->dimensions.colorBarButtonsHeight()));
308 w->setMaxSize(gfx::Size(std::numeric_limits<int>::max(),
309 theme->dimensions.colorBarButtonsHeight())); // TODO add resetMaxSize
310 }
311
312 m_buttons.setMaxSize(
313 gfx::Size(m_buttons.sizeHint().w,
314 theme->dimensions.colorBarButtonsHeight()));
315 m_tilesetModeButtons.setMaxSize(
316 gfx::Size(m_tilesetModeButtons.sizeHint().w,
317 theme->dimensions.colorBarButtonsHeight()));
318
319 // Change color-bar background color (not ColorBar::setBgColor)
320 this->Widget::setBgColor(theme->colors.tabActiveFace());
321 m_paletteView.setBgColor(theme->colors.tabActiveFace());
322 m_paletteView.setBoxSize(Preferences::instance().colorBar.boxSize());
323 m_paletteView.initTheme();
324
325 m_tilesView.setBgColor(theme->colors.tabActiveFace());
326 m_tilesView.setBoxSize(Preferences::instance().colorBar.tilesBoxSize());
327 m_tilesView.initTheme();
328
329 // Styles
330 m_splitter.setStyle(theme->styles.workspaceSplitter());
331 m_splitterPalTil.setStyle(theme->styles.workspaceSplitter());
332
333 fgBox->noBorderNoChildSpacing();
334 bgBox->noBorderNoChildSpacing();
335
336 if (m_palettePopup)
337 m_palettePopup->initTheme();
338 });
339 initTheme();
340
341 // Set background color reading its value from the configuration.
342 setBgColor(Preferences::instance().colorBar.bgColor());
343
344 // Clear the selection of the BG color in the palette.
345 m_paletteView.deselect();
346
347 // Set foreground color reading its value from the configuration.
348 setFgColor(Preferences::instance().colorBar.fgColor());
349
350 // Tooltips
351 setupTooltips(tooltipManager);
352
353 onColorButtonChange(getFgColor());
354
355 UIContext::instance()->add_observer(this);
356 m_beforeCmdConn = UIContext::instance()->BeforeCommandExecution.connect(&ColorBar::onBeforeExecuteCommand, this);
357 m_afterCmdConn = UIContext::instance()->AfterCommandExecution.connect(&ColorBar::onAfterExecuteCommand, this);
358 m_fgConn = Preferences::instance().colorBar.fgColor.AfterChange.connect([this]{ onFgColorChangeFromPreferences(); });
359 m_bgConn = Preferences::instance().colorBar.bgColor.AfterChange.connect([this]{ onBgColorChangeFromPreferences(); });
360 m_fgTileConn = Preferences::instance().colorBar.fgTile.AfterChange.connect([this]{ onFgTileChangeFromPreferences(); });
361 m_bgTileConn = Preferences::instance().colorBar.bgTile.AfterChange.connect([this]{ onBgTileChangeFromPreferences(); });
362 m_sepConn = Preferences::instance().colorBar.entriesSeparator.AfterChange.connect([this]{ invalidate(); });
363 m_paletteView.FocusOrClick.connect(&ColorBar::onFocusPaletteView, this);
364 m_tilesView.FocusOrClick.connect(&ColorBar::onFocusTilesView, this);
365 m_appPalChangeConn = App::instance()->PaletteChange.connect(&ColorBar::onAppPaletteChange, this);
366 KeyboardShortcuts::instance()->UserChange.connect(
367 [this, tooltipManager]{ setupTooltips(tooltipManager); });
368
369 setEditMode(false);
370 registerCommands();
371}
372
373ColorBar::~ColorBar()
374{
375 UIContext::instance()->remove_observer(this);
376}
377
378void ColorBar::setPixelFormat(PixelFormat pixelFormat)
379{
380 m_fgColor.setPixelFormat(pixelFormat);
381 m_bgColor.setPixelFormat(pixelFormat);
382}
383
384app::Color ColorBar::getFgColor() const
385{
386 return m_fgColor.getColor();
387}
388
389app::Color ColorBar::getBgColor() const
390{
391 return m_bgColor.getColor();
392}
393
394void ColorBar::setFgColor(const app::Color& color)
395{
396 if (m_fromFgButton)
397 return;
398
399 m_fgColor.setColor(color);
400 if (!m_fromPalView)
401 onColorButtonChange(color);
402}
403
404void ColorBar::setBgColor(const app::Color& color)
405{
406 if (m_fromBgButton)
407 return;
408
409 m_bgColor.setColor(color);
410 if (!m_fromPalView)
411 onColorButtonChange(color);
412}
413
414void ColorBar::setFgTile(doc::tile_t tile)
415{
416 m_fgTile.setTile(tile);
417 m_tilesView.selectColor(tile);
418 if (!m_fromPalView)
419 onFgTileButtonChange(tile);
420}
421
422void ColorBar::setBgTile(doc::tile_t tile)
423{
424 m_bgTile.setTile(tile);
425 m_tilesView.selectColor(tile);
426 if (!m_fromPalView)
427 onBgTileButtonChange(tile);
428}
429
430doc::tile_index ColorBar::getFgTile() const
431{
432 return m_fgTile.getTile();
433}
434
435doc::tile_index ColorBar::getBgTile() const
436{
437 return m_bgTile.getTile();
438}
439
440ColorBar::ColorSelector ColorBar::getColorSelector() const
441{
442 return m_selector;
443}
444
445void ColorBar::setColorSelector(ColorSelector selector)
446{
447 if (m_selector == selector)
448 return;
449
450 if (m_tintShadeTone) m_tintShadeTone->setVisible(false);
451 if (m_spectrum) m_spectrum->setVisible(false);
452 if (m_wheel) m_wheel->setVisible(false);
453
454 m_selector = selector;
455 Preferences::instance().colorBar.selector(m_selector);
456
457 switch (m_selector) {
458
459 case ColorSelector::TINT_SHADE_TONE:
460 if (!m_tintShadeTone) {
461 m_tintShadeTone = new ColorTintShadeTone;
462 m_tintShadeTone->setExpansive(true);
463 m_tintShadeTone->selectColor(m_fgColor.getColor());
464 m_tintShadeTone->ColorChange.connect(&ColorBar::onPickSpectrum, this);
465 m_selectorPlaceholder.addChild(m_tintShadeTone);
466 }
467 m_tintShadeTone->setVisible(true);
468 break;
469
470 case ColorSelector::SPECTRUM:
471 if (!m_spectrum) {
472 m_spectrum = new ColorSpectrum;
473 m_spectrum->setExpansive(true);
474 m_spectrum->selectColor(m_fgColor.getColor());
475 m_spectrum->ColorChange.connect(&ColorBar::onPickSpectrum, this);
476 m_selectorPlaceholder.addChild(m_spectrum);
477 }
478 m_spectrum->setVisible(true);
479 break;
480
481 case ColorSelector::RGB_WHEEL:
482 case ColorSelector::RYB_WHEEL:
483 case ColorSelector::NORMAL_MAP_WHEEL:
484 if (!m_wheel) {
485 m_wheel = new ColorWheel;
486 m_wheel->setExpansive(true);
487 m_wheel->selectColor(m_fgColor.getColor());
488 m_wheel->ColorChange.connect(&ColorBar::onPickSpectrum, this);
489 m_selectorPlaceholder.addChild(m_wheel);
490 }
491 if (m_selector == ColorSelector::RGB_WHEEL) {
492 m_wheel->setColorModel(ColorWheel::ColorModel::RGB);
493 }
494 else if (m_selector == ColorSelector::RYB_WHEEL) {
495 m_wheel->setColorModel(ColorWheel::ColorModel::RYB);
496 }
497 else if (m_selector == ColorSelector::NORMAL_MAP_WHEEL) {
498 m_wheel->setColorModel(ColorWheel::ColorModel::NORMAL_MAP);
499 }
500 m_wheel->setVisible(true);
501 break;
502
503 }
504
505 m_selectorPlaceholder.layout();
506}
507
508bool ColorBar::inEditMode() const
509{
510 return
511 (m_editMode &&
512 m_lastDocument &&
513 m_lastDocument->sprite() &&
514 m_lastDocument->sprite()->pixelFormat() != IMAGE_GRAYSCALE);
515}
516
517void ColorBar::setEditMode(bool state)
518{
519 auto theme = SkinTheme::get(this);
520 ButtonSet::Item* item = m_editPal.getItem(0);
521
522 m_editMode = state;
523 item->setIcon(state ? theme->parts.timelineOpenPadlockActive():
524 theme->parts.timelineClosedPadlockNormal());
525 item->setHotColor(state ? theme->colors.editPalFace(): gfx::ColorNone);
526
527 // Deselect color entries when we cancel editing
528 if (!state)
529 m_paletteView.deselect();
530}
531
532TilemapMode ColorBar::tilemapMode() const
533{
534 return
535 (m_lastDocument &&
536 m_lastDocument->sprite()) ? m_tilemapMode:
537 TilemapMode::Pixels;
538}
539
540void ColorBar::setTilemapMode(TilemapMode mode)
541{
542 if (m_tilemapMode != mode) {
543 m_tilemapMode = mode;
544 updateFromTilemapMode();
545 }
546}
547
548void ColorBar::updateFromTilemapMode()
549{
550 SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
551 ButtonSet::Item* item = m_tilesButton.getItem(0);
552
553 const bool canEditTiles = this->canEditTiles();
554 const bool editTiles = (canEditTiles &&
555 m_tilemapMode == TilemapMode::Tiles);
556
557 item->setHotColor(editTiles ? theme->colors.editPalFace():
558 gfx::ColorNone);
559 item->setMono(true);
560
561 if (Preferences::instance().colorBar.showColorAndTiles()) {
562 m_scrollablePalView.setVisible(true);
563 m_selectorPlaceholder.setVisible(true);
564 if (canEditTiles) {
565 m_scrollableTilesView.setVisible(true);
566 }
567 else {
568 manager()->freeWidget(&m_tilesView);
569 m_scrollableTilesView.setVisible(false);
570 }
571
572 if (editTiles) {
573 m_colorHelpers.setVisible(false);
574 m_tilesHelpers.setVisible(true);
575 }
576 else {
577 m_colorHelpers.setVisible(true);
578 m_tilesHelpers.setVisible(false);
579 }
580 }
581 else {
582 if (editTiles) {
583 manager()->freeWidget(&m_paletteView);
584 m_scrollablePalView.setVisible(false);
585 m_scrollableTilesView.setVisible(true);
586 m_selectorPlaceholder.setVisible(false);
587 m_colorHelpers.setVisible(false);
588 m_tilesHelpers.setVisible(true);
589 }
590 else {
591 manager()->freeWidget(&m_tilesView);
592 m_scrollablePalView.setVisible(true);
593 m_scrollableTilesView.setVisible(false);
594 m_selectorPlaceholder.setVisible(true);
595 m_colorHelpers.setVisible(true);
596 m_tilesHelpers.setVisible(false);
597 }
598 }
599
600 layout();
601}
602
603TilesetMode ColorBar::tilesetMode() const
604{
605 if (m_lastDocument &&
606 m_lastDocument->sprite()) {
607 return m_tilesetMode;
608 }
609 else
610 return TilesetMode::Manual;
611}
612
613void ColorBar::setTilesetMode(const TilesetMode mode)
614{
615 m_tilesetMode = mode;
616
617 for (int i=0; i<3; ++i) {
618 ButtonSet::Item* item = m_tilesetModeButtons.getItem(i);
619 item->setSelected(int(mode) == i);
620 }
621
622 // Change to pixels mode automatically
623 if (m_tilemapMode == TilemapMode::Tiles)
624 setTilemapMode(TilemapMode::Pixels);
625}
626
627void ColorBar::onSizeHint(ui::SizeHintEvent& ev)
628{
629 m_colorHelpers.resetSizeHint();
630 m_tilesHelpers.resetSizeHint();
631 gfx::Size sz = m_colorHelpers.sizeHint();
632 sz |= m_tilesHelpers.sizeHint();
633 m_colorHelpers.setSizeHint(sz);
634 m_tilesHelpers.setSizeHint(sz);
635
636 Box::onSizeHint(ev);
637}
638
639void ColorBar::onActiveSiteChange(const Site& site)
640{
641 if (m_lastDocument != site.document()) {
642 if (m_lastDocument)
643 m_lastDocument->remove_observer(this);
644
645 m_lastDocument = const_cast<Doc*>(site.document());
646
647 if (m_lastDocument)
648 m_lastDocument->add_observer(this);
649
650 hideRemapPal();
651 hideRemapTiles();
652 }
653
654 bool isTilemap = false;
655 if (site.layer())
656 isTilemap = site.layer()->isTilemap();
657
658 if (m_tilesHBox.isVisible() != isTilemap) {
659 m_tilesHBox.setVisible(isTilemap);
660 updateFromTilemapMode();
661 }
662
663 if (isTilemap) {
664 doc::ObjectId newTilesetId =
665 static_cast<const doc::LayerTilemap*>(site.layer())->tileset()->id();
666 if (m_lastTilesetId != newTilesetId) {
667 m_lastTilesetId = newTilesetId;
668 m_scrollableTilesView.updateView();
669 }
670 }
671 else {
672 m_lastTilesetId = doc::NullId;
673 }
674}
675
676void ColorBar::onGeneralUpdate(DocEvent& ev)
677{
678 // TODO Observe palette changes only
679 invalidate();
680}
681
682void ColorBar::onTilesetChanged(DocEvent& ev)
683{
684 // This can happen when a filter is applied to each tile in a
685 // background thread.
686 if (!ui::is_ui_thread())
687 return;
688
689 if (m_scrollableTilesView.isVisible())
690 m_scrollableTilesView.updateView();
691
692 m_tilesView.deselect();
693}
694
695void ColorBar::onAppPaletteChange()
696{
697 COLOR_BAR_TRACE("ColorBar::onAppPaletteChange()\n");
698
699 fixColorIndex(m_fgColor);
700 fixColorIndex(m_bgColor);
701
702 updateWarningIcon(m_fgColor.getColor(), m_fgWarningIcon);
703 updateWarningIcon(m_bgColor.getColor(), m_bgWarningIcon);
704}
705
706void ColorBar::onFocusPaletteView(ui::Message* msg)
707{
708 if (tilemapMode() != TilemapMode::Pixels)
709 setTilemapMode(TilemapMode::Pixels);
710 App::instance()->inputChain().prioritize(this, msg);
711}
712
713void ColorBar::onFocusTilesView(ui::Message* msg)
714{
715 if (tilemapMode() != TilemapMode::Tiles)
716 setTilemapMode(TilemapMode::Tiles);
717 App::instance()->inputChain().prioritize(this, msg);
718}
719
720void ColorBar::onBeforeExecuteCommand(CommandExecutionEvent& ev)
721{
722 if (ev.command()->id() == CommandId::SetPalette() ||
723 ev.command()->id() == CommandId::LoadPalette() ||
724 ev.command()->id() == CommandId::ColorQuantization()) {
725 showRemapPal();
726 }
727}
728
729void ColorBar::onAfterExecuteCommand(CommandExecutionEvent& ev)
730{
731 if (ev.command()->id() == CommandId::Undo() ||
732 ev.command()->id() == CommandId::Redo())
733 invalidate();
734
735 // If the sprite isn't Indexed anymore (e.g. because we've just
736 // undone a "RGB -> Indexed" conversion), we hide the "Remap
737 // Palette" button.
738 Site site = UIContext::instance()->activeSite();
739 if (site.sprite() &&
740 site.sprite()->pixelFormat() != IMAGE_INDEXED) {
741 hideRemapPal();
742 }
743
744 // If the layer isn't a tilemap anymore, we hide the "Remap Tiles"
745 // button.
746 if (site.layer() &&
747 !site.layer()->isTilemap()) {
748 hideRemapTiles();
749 }
750}
751
752void ColorBar::onSwitchPalEditMode()
753{
754 m_editPal.deselectItems();
755 setEditMode(!inEditMode());
756}
757
758// Switches the palette-editor
759void ColorBar::onPaletteButtonClick()
760{
761 int item = m_buttons.selectedItem();
762 m_buttons.deselectItems();
763
764 switch (static_cast<PalButton>(item)) {
765
766 case PalButton::SORT:
767 showPaletteSortOptions();
768 break;
769
770 case PalButton::PRESETS:
771 showPalettePresets();
772 break;
773
774 case PalButton::OPTIONS:
775 showPaletteOptions();
776 break;
777
778 }
779}
780
781void ColorBar::onTilesButtonClick()
782{
783 m_tilesButton.deselectItems();
784 setTilemapMode(
785 (tilemapMode() == TilemapMode::Pixels ? TilemapMode::Tiles:
786 TilemapMode::Pixels));
787}
788
789void ColorBar::onTilesButtonRightClick()
790{
791 auto& pref = Preferences::instance();
792 pref.colorBar.showColorAndTiles(!pref.colorBar.showColorAndTiles());
793 updateFromTilemapMode();
794}
795
796void ColorBar::onTilesetModeButtonClick()
797{
798 int item = m_tilesetModeButtons.selectedItem();
799 m_tilesetModeButtons.deselectItems();
800 setTilesetMode(static_cast<TilesetMode>(item));
801}
802
803void ColorBar::onRemapPalButtonClick()
804{
805 ASSERT(m_oldPalette);
806
807 // Create remap from m_oldPalette to the current palette
808 Remap remap(1);
809 try {
810 InlineCommandExecution inlineCmd(UIContext::instance());
811 ContextWriter writer(UIContext::instance());
812 Sprite* sprite = writer.sprite();
813 ASSERT(sprite);
814 if (!sprite)
815 return;
816
817 remap = create_remap_to_change_palette(
818 m_oldPalette.get(), get_current_palette(),
819 sprite->transparentColor(), true);
820 }
821 catch (base::Exception& e) {
822 Console::showException(e);
823 }
824
825 // Check the remap
826 if (!remap.isFor8bit() &&
827 Alert::show(Strings::alerts_auto_remap()) != 1) {
828 return;
829 }
830
831 try {
832 InlineCommandExecution inlineCmd(UIContext::instance());
833 ContextWriter writer(UIContext::instance());
834 Sprite* sprite = writer.sprite();
835 if (sprite) {
836 ASSERT(sprite->pixelFormat() == IMAGE_INDEXED);
837
838 Tx tx(writer.context(), "Remap Colors", ModifyDocument);
839 bool remapPixels = true;
840
841 std::vector<ImageRef> images;
842 sprite->getImages(images);
843
844 if (remap.isFor8bit()) {
845 PalettePicks usedEntries(256);
846
847 for (ImageRef& image : images) {
848 for (const auto& i : LockImageBits<IndexedTraits>(image.get()))
849 usedEntries[i] = true;
850 }
851
852 if (remap.isInvertible(usedEntries)) {
853 for (int i=0; i<remap.size(); ++i) {
854 if (i >= usedEntries.size() || !usedEntries[i]) {
855 remap.unused(i);
856 }
857 }
858
859 tx(new cmd::RemapColors(sprite, remap));
860 remapPixels = false;
861 }
862 }
863
864 // Special remap saving original images in undo history
865 if (remapPixels) {
866 for (ImageRef& image : images) {
867 ImageRef newImage(Image::createCopy(image.get()));
868 doc::remap_image(newImage.get(), remap);
869
870 tx(new cmd::ReplaceImage(sprite, image, newImage));
871 }
872 }
873
874 color_t oldTransparent = sprite->transparentColor();
875 color_t newTransparent = (remap[oldTransparent] >= 0) ? remap[oldTransparent] : oldTransparent;
876 if (newTransparent >= get_current_palette()->size())
877 newTransparent = get_current_palette()->size() - 1;
878 if (oldTransparent != newTransparent)
879 tx(new cmd::SetTransparentColor(sprite, newTransparent));
880
881 tx.commit();
882 }
883 update_screen_for_document(writer.document());
884 hideRemapPal();
885 }
886 catch (base::Exception& e) {
887 Console::showException(e);
888 }
889}
890
891void ColorBar::onRemapTilesButtonClick()
892{
893 COLOR_BAR_TRACE("remapTiles\n");
894 try {
895 InlineCommandExecution inlineCmd(UIContext::instance());
896 ContextWriter writer(UIContext::instance(), 500);
897 Sprite* sprite = writer.sprite();
898 if (!sprite)
899 return;
900
901 doc::Tileset* tileset = m_tilesView.tileset();
902 std::vector<ImageRef> tilemaps;
903 sprite->getTilemapsByTileset(tileset, tilemaps);
904
905 const int n = std::max(m_oldTileset->size(),
906 tileset->size());
907 PalettePicks usedTiles(n);
908
909 if (n > 0) {
910 for (const ImageRef& tilemap : tilemaps) {
911 for (const doc::tile_t t : LockImageBits<TilemapTraits>(tilemap.get()))
912 if (t != doc::notile)
913 usedTiles[doc::tile_geti(t)] = true;
914 }
915 }
916
917 // Remap all tiles in the same order as in newTileset
918 Remap remap(n);
919 bool existMapToEmpty = false;
920
921 for (tile_index ti=0; ti<n; ++ti) {
922 auto img = m_oldTileset->get(ti);
923 tile_index destTi;
924 if (img) {
925 tileset->findTileIndex(img, destTi);
926
927 COLOR_BAR_TRACE(" - Remap tile %d -> %d\n", ti, destTi);
928 remap.map(ti, destTi);
929 }
930 else {
931 remap.map(ti, destTi = doc::notile);
932 }
933
934 if (destTi == doc::notile &&
935 ti < usedTiles.size() && usedTiles[ti]) {
936 COLOR_BAR_TRACE(" - Remap tile %d to empty (used=%d)\n", ti, usedTiles[ti]);
937 existMapToEmpty = true;
938 }
939 }
940
941 // Nothing to remap
942 if (remap.isIdentity()) {
943 COLOR_BAR_TRACE(" - Nothing to remap\n");
944 return;
945 }
946
947 Tx tx(writer.context(), Strings::color_bar_remap_tiles(), ModifyDocument);
948 if (!existMapToEmpty &&
949 remap.isInvertible(usedTiles)) {
950 tx(new cmd::RemapTilemaps(tileset, remap));
951 }
952 else {
953 for (const ImageRef& tilemap : tilemaps) {
954 ImageRef newTilemap(Image::createCopy(tilemap.get()));
955 doc::remap_image(newTilemap.get(), remap);
956
957 // TODO improve this with a cmd::CopyRegion()
958 tx(new cmd::ReplaceImage(sprite, tilemap, newTilemap));
959 }
960 }
961 tx.commit();
962
963 hideRemapTiles();
964 // TODO this should be automatic in last ~Tx() destruction
965 manager()->invalidate();
966 }
967 catch (base::Exception& e) {
968 Console::showException(e);
969 }
970}
971
972bool ColorBar::onIsPaletteViewActive(PaletteView* paletteView) const
973{
974 if (paletteView == &m_paletteView) {
975 return
976 (m_tilemapMode == TilemapMode::Pixels) ||
977 // As the m_tilemapMode value is kept to restore it if we go
978 // back to a tilemap layer, there is a possibility where we are
979 // in a regular layer with the tiles mode enabled (because we
980 // were in a tilemap layer just right before). We have to check
981 // this special case where we are in "tiles mode" but we are
982 // actually in a regular layer (canEditTiles() is false).
983 (m_tilemapMode == TilemapMode::Tiles && !canEditTiles());
984 }
985 else if (paletteView == &m_tilesView) {
986 return (m_tilemapMode == TilemapMode::Tiles);
987 }
988 else {
989 return false;
990 }
991}
992
993void ColorBar::onPaletteViewIndexChange(int index, ui::MouseButton button)
994{
995 COLOR_BAR_TRACE("ColorBar::onPaletteViewIndexChange(%d)\n", index);
996
997 base::ScopedValue<bool> lock(m_fromPalView, true, m_fromPalView);
998
999 app::Color color = app::Color::fromIndex(index);
1000
1001 if (button == kButtonRight)
1002 setBgColor(color);
1003 else if (button == kButtonLeft)
1004 setFgColor(color);
1005 else if (button == kButtonMiddle)
1006 setTransparentIndex(index);
1007
1008 ChangeSelection();
1009}
1010
1011void ColorBar::onPaletteViewModification(const Palette* newPalette,
1012 PaletteViewModification mod)
1013{
1014 const char* text = "Palette Change";
1015 switch (mod) {
1016 case PaletteViewModification::CLEAR: text = "Clear Colors"; break;
1017 case PaletteViewModification::DRAGANDDROP: text = "Drag-and-Drop Colors"; break;
1018 case PaletteViewModification::RESIZE: text = "Resize Palette"; break;
1019 }
1020 setPalette(newPalette, text);
1021}
1022
1023void ColorBar::setPalette(const doc::Palette* newPalette, const std::string& actionText)
1024{
1025 showRemapPal();
1026
1027 try {
1028 InlineCommandExecution inlineCmd(UIContext::instance());
1029 ContextWriter writer(UIContext::instance(), 500);
1030 Sprite* sprite = writer.sprite();
1031 frame_t frame = writer.frame();
1032 if (sprite &&
1033 newPalette->countDiff(sprite->palette(frame), nullptr, nullptr)) {
1034 Tx tx(writer.context(), actionText, ModifyDocument);
1035 tx(new cmd::SetPalette(sprite, frame, newPalette));
1036 tx.commit();
1037 }
1038 }
1039 catch (base::Exception& e) {
1040 Console::showException(e);
1041 }
1042}
1043
1044void ColorBar::setTransparentIndex(int index)
1045{
1046 try {
1047 InlineCommandExecution inlineCmd(UIContext::instance());
1048 ContextWriter writer(UIContext::instance());
1049 Sprite* sprite = writer.sprite();
1050 if (sprite &&
1051 sprite->pixelFormat() == IMAGE_INDEXED &&
1052 int(sprite->transparentColor()) != index) {
1053 // TODO merge this code with SpritePropertiesCommand
1054 Tx tx(writer.context(), "Set Transparent Color");
1055 DocApi api = writer.document()->getApi(tx);
1056 api.setSpriteTransparentColor(sprite, index);
1057 tx.commit();
1058
1059 update_screen_for_document(writer.document());
1060 }
1061 }
1062 catch (base::Exception& e) {
1063 Console::showException(e);
1064 }
1065}
1066
1067void ColorBar::onPaletteViewChangeSize(PaletteView* paletteView, int boxsize)
1068{
1069 if (paletteView == &m_tilesView)
1070 Preferences::instance().colorBar.tilesBoxSize(boxsize);
1071 else
1072 Preferences::instance().colorBar.boxSize(boxsize);
1073}
1074
1075void ColorBar::onPaletteViewPasteColors(
1076 const Palette* fromPal, const doc::PalettePicks& from, const doc::PalettePicks& _to)
1077{
1078 if (!from.picks() || !_to.picks()) // Nothing to do
1079 return;
1080
1081 doc::PalettePicks to = _to;
1082 int to_first = to.firstPick();
1083 int to_last = to.lastPick();
1084
1085 // Add extra picks in to range if it's needed to paste more colors.
1086 int from_picks = from.picks();
1087 int to_picks = to.picks();
1088 if (to_picks < from_picks) {
1089 for (int j=to_last+1; j<to.size() && to_picks<from_picks; ++j) {
1090 to[j] = true;
1091 ++to_picks;
1092 }
1093 }
1094
1095 Palette newPalette(*get_current_palette());
1096
1097 int i = 0;
1098 int j = to_first;
1099
1100 for (auto state : from) {
1101 if (state) {
1102 if (j < newPalette.size())
1103 newPalette.setEntry(j, fromPal->getEntry(i));
1104 else
1105 newPalette.addEntry(fromPal->getEntry(i));
1106
1107 for (++j; j<to.size(); ++j)
1108 if (to[j])
1109 break;
1110 }
1111 ++i;
1112 }
1113
1114 setPalette(&newPalette, "Paste Colors");
1115}
1116
1117app::Color ColorBar::onPaletteViewGetForegroundIndex()
1118{
1119 return getFgColor();
1120}
1121
1122app::Color ColorBar::onPaletteViewGetBackgroundIndex()
1123{
1124 return getBgColor();
1125}
1126
1127doc::tile_index ColorBar::onPaletteViewGetForegroundTile()
1128{
1129 return doc::tile_geti(getFgTile());
1130}
1131
1132doc::tile_index ColorBar::onPaletteViewGetBackgroundTile()
1133{
1134 return doc::tile_geti(getBgTile());
1135}
1136
1137void ColorBar::onTilesViewClearTiles(const doc::PalettePicks& _picks)
1138{
1139 // Copy the collection of selected tiles because in case that the
1140 // user want to delete a range of tiles (several tiles at the same
1141 // time), after the first cmd::RemoveTile() is executed this
1142 // collection (_picks) will be modified and we'll lost the other
1143 // selected tiles to remove.
1144 doc::PalettePicks picks = _picks;
1145 try {
1146 InlineCommandExecution inlineCmd(UIContext::instance());
1147 ContextWriter writer(UIContext::instance(), 500);
1148 Sprite* sprite = writer.sprite();
1149 ASSERT(writer.layer()->isTilemap());
1150 if (sprite) {
1151 auto tileset = m_tilesView.tileset();
1152
1153 Tx tx(writer.context(), "Clear Tiles", ModifyDocument);
1154 for (int ti=int(picks.size())-1; ti>=0; --ti) {
1155 if (picks[ti])
1156 tx(new cmd::RemoveTile(tileset, ti));
1157 }
1158 tx.commit();
1159
1160 update_screen_for_document(writer.document());
1161 }
1162 }
1163 catch (base::Exception& e) {
1164 Console::showException(e);
1165 }
1166}
1167
1168void ColorBar::onTilesViewResize(const int newSize)
1169{
1170 auto tileset = m_tilesView.tileset();
1171 if (!tileset || tileset->size() == newSize)
1172 return;
1173
1174 showRemapTiles();
1175
1176 try {
1177 InlineCommandExecution inlineCmd(UIContext::instance());
1178 ContextWriter writer(UIContext::instance(), 500);
1179 Sprite* sprite = writer.sprite();
1180 ASSERT(writer.layer()->isTilemap());
1181 if (sprite) {
1182 auto tileset = m_tilesView.tileset();
1183
1184 Tx tx(writer.context(), Strings::color_bar_resize_tiles(), ModifyDocument);
1185 if (tileset->size() < newSize) {
1186 for (doc::tile_index ti=tileset->size(); ti<newSize; ++ti) {
1187 ImageRef img = tileset->makeEmptyTile();
1188 tx(new cmd::AddTile(tileset, img));
1189 }
1190 }
1191 else {
1192 for (doc::tile_index ti=tileset->size()-1;
1193 ti!=(doc::tile_index)newSize-1; --ti) {
1194 tx(new cmd::RemoveTile(tileset, ti));
1195 }
1196 }
1197
1198 tx.commit();
1199
1200 // TODO this should be automatic (when tileset is changed after a transaction)
1201 m_scrollableTilesView.updateView();
1202 update_screen_for_document(writer.document());
1203 }
1204 }
1205 catch (base::Exception& e) {
1206 Console::showException(e);
1207 }
1208}
1209
1210void ColorBar::onTilesViewDragAndDrop(doc::Tileset* tileset,
1211 doc::PalettePicks& picks,
1212 int& currentEntry,
1213 const int beforeIndex,
1214 const bool isCopy)
1215{
1216 COLOR_BAR_TRACE("ColorBar::onTilesViewDragAndDrop() -> beforeIndex=%d\n",
1217 beforeIndex);
1218
1219 showRemapTiles();
1220
1221 try {
1222 Context* ctx = UIContext::instance();
1223 InlineCommandExecution inlineCmd(ctx);
1224 ContextWriter writer(ctx, 500);
1225 Tx tx(writer.context(), Strings::color_bar_drag_and_drop_tiles(), ModifyDocument);
1226 if (isCopy)
1227 copy_tiles_in_tileset(tx, tileset, picks, currentEntry, beforeIndex);
1228 else
1229 move_tiles_in_tileset(tx, tileset, picks, currentEntry, beforeIndex);
1230 tx.commit();
1231
1232 m_scrollableTilesView.updateView();
1233 update_screen_for_document(writer.document());
1234
1235 ctx->setSelectedTiles(picks);
1236 }
1237 catch (base::Exception& e) {
1238 Console::showException(e);
1239 }
1240}
1241
1242void ColorBar::onTilesViewIndexChange(int index, ui::MouseButton button)
1243{
1244 auto& pref = Preferences::instance();
1245 if (button == kButtonRight)
1246 pref.colorBar.bgTile(doc::tile(index, 0));
1247 else if (button == kButtonLeft)
1248 pref.colorBar.fgTile(doc::tile(index, 0));
1249 else if (button == kButtonMiddle) {
1250 // TODO ?
1251 }
1252}
1253
1254void ColorBar::onFgColorChangeFromPreferences()
1255{
1256 COLOR_BAR_TRACE("ColorBar::onFgColorChangeFromPreferences() -> %s\n",
1257 Preferences::instance().colorBar.fgColor().toString().c_str());
1258
1259 if (m_fromPref)
1260 return;
1261
1262 base::ScopedValue<bool> sync(m_fromPref, true, false);
1263 setFgColor(Preferences::instance().colorBar.fgColor());
1264}
1265
1266void ColorBar::onBgColorChangeFromPreferences()
1267{
1268 COLOR_BAR_TRACE("ColorBar::onBgColorChangeFromPreferences() -> %s\n",
1269 Preferences::instance().colorBar.bgColor().toString().c_str());
1270
1271 if (m_fromPref)
1272 return;
1273
1274 if (inEditMode()) {
1275 // In edit mode, clicking with right-click will copy the color
1276 // selected with eyedropper to the active color entry.
1277 setFgColor(Preferences::instance().colorBar.bgColor());
1278 }
1279 else {
1280 base::ScopedValue<bool> sync(m_fromPref, true, false);
1281 setBgColor(Preferences::instance().colorBar.bgColor());
1282 }
1283}
1284
1285void ColorBar::onFgTileChangeFromPreferences()
1286{
1287 if (m_fromPref)
1288 return;
1289
1290 base::ScopedValue<bool> sync(m_fromPref, true, false);
1291 auto tile = Preferences::instance().colorBar.fgTile();
1292 m_fgTile.setTile(tile);
1293 m_tilesView.selectColor(tile);
1294}
1295
1296void ColorBar::onBgTileChangeFromPreferences()
1297{
1298 if (m_fromPref)
1299 return;
1300
1301 base::ScopedValue<bool> sync(m_fromPref, true, false);
1302 auto tile = Preferences::instance().colorBar.bgTile();
1303 m_bgTile.setTile(tile);
1304 m_tilesView.selectColor(tile);
1305}
1306
1307void ColorBar::onFgColorButtonBeforeChange(app::Color& color)
1308{
1309 COLOR_BAR_TRACE("ColorBar::onFgColorButtonBeforeChange(%s)\n", color.toString().c_str());
1310
1311 if (m_fromPalView)
1312 return;
1313
1314 if (!inEditMode() || color.getType() == app::Color::IndexType) {
1315 m_paletteView.deselect();
1316 return;
1317 }
1318
1319 // Here we change the selected colors in the
1320 // palette. "m_fromPref" must be false to edit the color. (It
1321 // means, if the eyedropper was used with the left-click, we don't
1322 // edit the color, we just select the color to as the normal
1323 // non-edit mode.)
1324 if (!m_fromPref) {
1325 int i = setPaletteEntry(color);
1326 if (i >= 0) {
1327 updateCurrentSpritePalette("Color Change");
1328 color = app::Color::fromIndex(i);
1329 }
1330 }
1331}
1332
1333void ColorBar::onFgColorButtonChange(const app::Color& color)
1334{
1335 COLOR_BAR_TRACE("ColorBar::onFgColorButtonChange(%s)\n", color.toString().c_str());
1336
1337 if (m_fromFgButton)
1338 return;
1339
1340 base::ScopedValue<bool> lock(m_fromFgButton, true, false);
1341
1342 if (!m_fromPref) {
1343 base::ScopedValue<bool> sync(m_fromPref, true, false);
1344 Preferences::instance().colorBar.fgColor(color);
1345 }
1346
1347 updateWarningIcon(color, m_fgWarningIcon);
1348 onColorButtonChange(color);
1349}
1350
1351void ColorBar::onBgColorButtonChange(const app::Color& color)
1352{
1353 COLOR_BAR_TRACE("ColorBar::onBgColorButtonChange(%s)\n", color.toString().c_str());
1354
1355 if (m_fromBgButton)
1356 return;
1357
1358 base::ScopedValue<bool> lock(m_fromBgButton, true, false);
1359
1360 if (!m_fromPalView && !inEditMode())
1361 m_paletteView.deselect();
1362
1363 if (!m_fromPref) {
1364 base::ScopedValue<bool> sync(m_fromPref, true, false);
1365 Preferences::instance().colorBar.bgColor(color);
1366 }
1367
1368 updateWarningIcon(color, m_bgWarningIcon);
1369 onColorButtonChange(color);
1370}
1371
1372void ColorBar::onColorButtonChange(const app::Color& color)
1373{
1374 COLOR_BAR_TRACE("ColorBar::onColorButtonChange(%s)\n", color.toString().c_str());
1375
1376 if (!inEditMode() || color.getType() == app::Color::IndexType || m_fromPref) {
1377 if (color.getType() == app::Color::IndexType)
1378 m_paletteView.selectColor(color.getIndex());
1379 else {
1380 m_paletteView.selectExactMatchColor(color);
1381
1382 // As foreground or background color changed, we've to redraw the
1383 // palette view fg/bg indicators.
1384 m_paletteView.invalidate();
1385 }
1386 }
1387
1388 if (m_tintShadeTone && m_tintShadeTone->isVisible())
1389 m_tintShadeTone->selectColor(color);
1390
1391 if (m_spectrum && m_spectrum->isVisible())
1392 m_spectrum->selectColor(color);
1393
1394 if (m_wheel && m_wheel->isVisible())
1395 m_wheel->selectColor(color);
1396}
1397
1398void ColorBar::onFgTileButtonChange(doc::tile_t tile)
1399{
1400 if (!m_fromPref) {
1401 Preferences::instance().colorBar.fgTile(tile);
1402 m_tilesView.deselect();
1403 }
1404}
1405
1406void ColorBar::onBgTileButtonChange(doc::tile_t tile)
1407{
1408 if (!m_fromPref) {
1409 Preferences::instance().colorBar.bgTile(tile);
1410 m_tilesView.deselect();
1411 }
1412}
1413
1414void ColorBar::onPickSpectrum(const app::Color& color, ui::MouseButton button)
1415{
1416 // Change to pixels mode automatically
1417 if (m_tilemapMode == TilemapMode::Tiles)
1418 setTilemapMode(TilemapMode::Pixels);
1419
1420 if (button == kButtonNone)
1421 button = m_lastButton;
1422
1423 if (button == kButtonRight)
1424 setBgColor(color);
1425 else if (button == kButtonLeft)
1426 setFgColor(color);
1427
1428 m_lastButton = button;
1429}
1430
1431void ColorBar::onReverseColors()
1432{
1433 doc::PalettePicks entries;
1434 m_paletteView.getSelectedEntries(entries);
1435
1436 entries.pickAllIfNeeded();
1437 int n = entries.picks();
1438
1439 std::vector<int> mapToOriginal(n); // Maps index from selectedPalette -> palette
1440 int i = 0, j = 0;
1441 for (bool state : entries) {
1442 if (state)
1443 mapToOriginal[j++] = i;
1444 ++i;
1445 }
1446
1447 Remap remap(get_current_palette()->size());
1448 i = 0;
1449 j = n;
1450 for (bool state : entries) {
1451 if (state)
1452 remap.map(i, mapToOriginal[--j]);
1453 else
1454 remap.map(i, i);
1455 ++i;
1456 }
1457
1458 Palette newPalette(*get_current_palette(), remap);
1459 setPalette(&newPalette, Strings::color_bar_reverse_colors());
1460}
1461
1462void ColorBar::onSortBy(SortPaletteBy channel)
1463{
1464 PalettePicks entries;
1465 m_paletteView.getSelectedEntries(entries);
1466
1467 entries.pickAllIfNeeded();
1468 int n = entries.picks();
1469
1470 // Create a "subpalette" with selected entries only.
1471 Palette palette(*get_current_palette());
1472 Palette selectedPalette(0, n);
1473 std::vector<int> mapToOriginal(n); // Maps index from selectedPalette -> palette
1474 int i = 0, j = 0;
1475 for (bool state : entries) {
1476 if (state) {
1477 selectedPalette.setEntry(j, palette.getEntry(i));
1478 mapToOriginal[j] = i;
1479 ++j;
1480 }
1481 ++i;
1482 }
1483
1484 // Create a remap to sort the selected entries with the given color
1485 // component/channel.
1486 Remap remap = doc::sort_palette(&selectedPalette, channel, m_ascending);
1487
1488 // Create a bigger new remap for the original palette (with all
1489 // entries, selected and deselected).
1490 Remap remapOrig(palette.size());
1491 i = j = 0;
1492 for (bool state : entries) {
1493 if (state)
1494 remapOrig.map(i, mapToOriginal[remap[j++]]);
1495 else
1496 remapOrig.map(i, i);
1497 ++i;
1498 }
1499
1500 // Create a new palette and apply the remap. This is the final new
1501 // palette for the sprite.
1502 Palette newPalette(palette, remapOrig);
1503 setPalette(&newPalette, Strings::color_bar_sort_colors());
1504}
1505
1506void ColorBar::onGradient(GradientType gradientType)
1507{
1508 int index1, index2;
1509 if (!m_paletteView.getSelectedRange(index1, index2))
1510 return;
1511
1512 Palette newPalette(*get_current_palette());
1513 if (gradientType == GradientType::LINEAR) {
1514 newPalette.makeGradient(index1, index2);
1515 setPalette(&newPalette, Strings::color_bar_gradient());
1516 }
1517 else {
1518 newPalette.makeHueGradient(index1, index2);
1519 setPalette(&newPalette, Strings::color_bar_gradient_by_hue());
1520 }
1521}
1522
1523void ColorBar::setAscending(bool ascending)
1524{
1525 m_ascending = ascending;
1526}
1527
1528void ColorBar::showRemapPal()
1529{
1530 Site site = UIContext::instance()->activeSite();
1531 if (site.sprite() &&
1532 site.sprite()->pixelFormat() == IMAGE_INDEXED) {
1533 if (!m_oldPalette) {
1534 m_oldPalette.reset(new Palette(*get_current_palette()));
1535 m_remapPalButton.setVisible(true);
1536 layout();
1537 }
1538 }
1539}
1540
1541void ColorBar::showRemapTiles()
1542{
1543 Site site = UIContext::instance()->activeSite();
1544 if (site.layer() &&
1545 site.layer()->isTilemap()) {
1546 if (!m_oldTileset) {
1547 m_oldTileset.reset(
1548 Tileset::MakeCopyCopyingImages(
1549 static_cast<LayerTilemap*>(site.layer())->tileset()));
1550 m_remapTilesButton.setVisible(true);
1551 layout();
1552 }
1553 }
1554}
1555
1556void ColorBar::hideRemapPal()
1557{
1558 if (!m_oldPalette)
1559 return;
1560
1561 m_oldPalette.reset();
1562 m_remapPalButton.setVisible(false);
1563 layout();
1564}
1565
1566void ColorBar::hideRemapTiles()
1567{
1568 if (!m_oldTileset)
1569 return;
1570
1571 m_oldTileset.reset();
1572 m_remapTilesButton.setVisible(false);
1573 layout();
1574}
1575
1576void ColorBar::onNewInputPriority(InputChainElement* element,
1577 const ui::Message* msg)
1578{
1579 if (dynamic_cast<Timeline*>(element) &&
1580 msg && (msg->ctrlPressed() || msg->shiftPressed()))
1581 return;
1582
1583 if (element != this) {
1584 m_paletteView.deselect();
1585 m_tilesView.deselect();
1586 }
1587}
1588
1589bool ColorBar::onCanCut(Context* ctx)
1590{
1591 if (m_tilemapMode == TilemapMode::Tiles)
1592 return (m_tilesView.getSelectedEntriesCount() > 0);
1593 else
1594 return (m_paletteView.getSelectedEntriesCount() > 0);
1595}
1596
1597bool ColorBar::onCanCopy(Context* ctx)
1598{
1599 return onCanCut(ctx);
1600}
1601
1602bool ColorBar::onCanPaste(Context* ctx)
1603{
1604 auto format = ctx->clipboard()->format();
1605 if (m_tilemapMode == TilemapMode::Tiles)
1606 return (format == ClipboardFormat::Tileset);
1607 else
1608 return (format == ClipboardFormat::PaletteEntries);
1609}
1610
1611bool ColorBar::onCanClear(Context* ctx)
1612{
1613 return onCanCut(ctx);
1614}
1615
1616bool ColorBar::onCut(Context* ctx)
1617{
1618 if (m_tilemapMode == TilemapMode::Tiles) {
1619 showRemapTiles();
1620 m_tilesView.cutToClipboard();
1621 }
1622 else
1623 m_paletteView.cutToClipboard();
1624 return true;
1625}
1626
1627bool ColorBar::onCopy(Context* ctx)
1628{
1629 if (m_tilemapMode == TilemapMode::Tiles)
1630 m_tilesView.copyToClipboard();
1631 else
1632 m_paletteView.copyToClipboard();
1633 return true;
1634}
1635
1636bool ColorBar::onPaste(Context* ctx)
1637{
1638 if (m_tilemapMode == TilemapMode::Tiles) {
1639 showRemapTiles();
1640 m_tilesView.pasteFromClipboard();
1641 }
1642 else
1643 m_paletteView.pasteFromClipboard();
1644 return true;
1645}
1646
1647bool ColorBar::onClear(Context* ctx)
1648{
1649 if (m_tilemapMode == TilemapMode::Tiles) {
1650 showRemapTiles();
1651 m_tilesView.clearSelection();
1652 }
1653 else
1654 m_paletteView.clearSelection();
1655 return true;
1656}
1657
1658void ColorBar::onCancel(Context* ctx)
1659{
1660 m_tilesView.deselect();
1661 m_tilesView.discardClipboardSelection();
1662 m_paletteView.deselect();
1663 m_paletteView.discardClipboardSelection();
1664 invalidate();
1665}
1666
1667void ColorBar::onFixWarningClick(ColorButton* colorButton, ui::Button* warningIcon)
1668{
1669 COLOR_BAR_TRACE("ColorBar::onFixWarningClick(%s)\n", colorButton->getColor().toString().c_str());
1670
1671 Palette* palette = get_current_palette();
1672 const int oldEntries = palette->size();
1673
1674 Command* command = Commands::instance()->byId(CommandId::AddColor());
1675 Params params;
1676 params.set("source", "color");
1677 params.set("color", colorButton->getColor().toString().c_str());
1678 UIContext::instance()->executeCommand(command, params);
1679
1680 // Select the new FG/BG color as an indexed color
1681 if (inEditMode()) {
1682 const int newEntries = palette->size();
1683 if (oldEntries != newEntries) {
1684 base::ScopedValue<bool> sync(m_fromPref, true, m_fromPref);
1685 app::Color newIndex = app::Color::fromIndex(newEntries-1);
1686 if (colorButton == &m_bgColor)
1687 setBgColor(newIndex);
1688 setFgColor(newIndex);
1689 }
1690 }
1691}
1692
1693void ColorBar::onTimerTick()
1694{
1695 // Redraw all editors
1696 if (m_redrawAll) {
1697 m_redrawAll = false;
1698 m_implantChange = false;
1699 m_redrawTimer.stop();
1700
1701 // Call all observers of PaletteChange event.
1702 m_selfPalChange = true;
1703 App::instance()->PaletteChange();
1704 m_selfPalChange = false;
1705
1706 // Redraw all editors
1707 try {
1708 InlineCommandExecution inlineCmd(UIContext::instance());
1709 ContextWriter writer(UIContext::instance());
1710 Doc* document(writer.document());
1711 if (document != NULL)
1712 document->notifyGeneralUpdate();
1713 }
1714 catch (...) {
1715 // Do nothing
1716 }
1717 }
1718 // Redraw just the current editor
1719 else {
1720 m_redrawAll = true;
1721 if (current_editor)
1722 current_editor->updateEditor(true);
1723 }
1724}
1725
1726void ColorBar::updateWarningIcon(const app::Color& color, ui::Button* warningIcon)
1727{
1728 int index = -1;
1729
1730 if (color.getType() == app::Color::MaskType) {
1731 if (current_editor &&
1732 current_editor->sprite()) {
1733 index = current_editor->sprite()->transparentColor();
1734 }
1735 else
1736 index = 0;
1737 }
1738 else {
1739 index = get_current_palette()->findExactMatch(
1740 color.getRed(),
1741 color.getGreen(),
1742 color.getBlue(),
1743 color.getAlpha(), -1);
1744 }
1745
1746 warningIcon->setVisible(index < 0);
1747 warningIcon->parent()->layout();
1748}
1749
1750// Changes the selected color palettes with the given
1751// app::Color. Returns the first modified index in the palette.
1752int ColorBar::setPaletteEntry(const app::Color& color)
1753{
1754 int selIdx = m_paletteView.getSelectedEntry();
1755 if (selIdx < 0) {
1756 if (getFgColor().getType() == app::Color::IndexType) {
1757 selIdx = getFgColor().getIndex();
1758 }
1759 }
1760
1761 PalettePicks entries;
1762 m_paletteView.getSelectedEntries(entries);
1763 if (entries.picks() == 0) {
1764 if (selIdx >= 0 && selIdx < entries.size()) {
1765 entries[selIdx] = true;
1766 }
1767 }
1768
1769 doc::color_t c =
1770 doc::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
1771
1772 Palette* palette = get_current_palette();
1773 for (int i=0; i<palette->size(); ++i) {
1774 if (entries[i])
1775 palette->setEntry(i, c);
1776 }
1777
1778 if (selIdx < 0 ||
1779 selIdx >= entries.size() ||
1780 !entries[selIdx])
1781 selIdx = entries.firstPick();
1782
1783 return selIdx;
1784}
1785
1786void ColorBar::updateCurrentSpritePalette(const char* operationName)
1787{
1788 if (UIContext::instance()->activeDocument() &&
1789 UIContext::instance()->activeDocument()->sprite()) {
1790 try {
1791 InlineCommandExecution inlineCmd(UIContext::instance());
1792 ContextWriter writer(UIContext::instance());
1793 Doc* document(writer.document());
1794 Sprite* sprite(writer.sprite());
1795 Palette* newPalette = get_current_palette(); // System current pal
1796 frame_t frame = writer.frame();
1797 Palette* currentSpritePalette = sprite->palette(frame); // Sprite current pal
1798 int from, to;
1799
1800 // Check differences between current sprite palette and current system palette
1801 from = to = -1;
1802 currentSpritePalette->countDiff(newPalette, &from, &to);
1803
1804 if (from >= 0 && to >= from) {
1805 DocUndo* undo = document->undoHistory();
1806 std::unique_ptr<cmd::SetPalette> cmd(
1807 new cmd::SetPalette(sprite, frame, newPalette));
1808
1809 // Add undo information to save the range of pal entries that will be modified.
1810 if (m_implantChange &&
1811 undo->lastExecutedCmd() &&
1812 undo->lastExecutedCmd()->label() == operationName) {
1813 // Implant the cmd in the last CmdSequence if it's
1814 // related about color palette modifications
1815 ASSERT(dynamic_cast<CmdSequence*>(undo->lastExecutedCmd()));
1816 static_cast<CmdSequence*>(undo->lastExecutedCmd())->add(cmd.get());
1817 // Release the unique pointer because it's already in the
1818 // last executed command CmdSequence::m_cmds container, and
1819 // execute it.
1820 cmd.release()->execute(UIContext::instance());
1821 }
1822 else {
1823 Tx tx(writer.context(), operationName, ModifyDocument);
1824 // If tx() fails it will delete the cmd anyway, so we can
1825 // release the unique pointer here.
1826 tx(cmd.release());
1827 tx.commit();
1828 }
1829 }
1830 }
1831 catch (base::Exception& e) {
1832 Console::showException(e);
1833 }
1834 }
1835
1836 m_paletteView.invalidate();
1837
1838 if (!m_redrawTimer.isRunning())
1839 m_redrawTimer.start();
1840
1841 m_redrawAll = false;
1842 m_implantChange = true;
1843}
1844
1845void ColorBar::setupTooltips(TooltipManager* tooltipManager)
1846{
1847 tooltipManager->addTooltipFor(&m_fgColor, Strings::color_bar_fg(), LEFT);
1848 tooltipManager->addTooltipFor(&m_bgColor, Strings::color_bar_bg(), LEFT);
1849 tooltipManager->addTooltipFor(m_fgWarningIcon, Strings::color_bar_fg_warning(), LEFT);
1850 tooltipManager->addTooltipFor(m_bgWarningIcon, Strings::color_bar_bg_warning(), LEFT);
1851
1852 tooltipManager->addTooltipFor(
1853 m_editPal.getItem(0),
1854 key_tooltip(Strings::color_bar_edit_color().c_str(), CommandId::PaletteEditor()),
1855 BOTTOM);
1856
1857 tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::SORT), Strings::color_bar_sort_and_gradients(), BOTTOM);
1858 tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::PRESETS), Strings::color_bar_presets(), BOTTOM);
1859 tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::OPTIONS), Strings::color_bar_options(), BOTTOM);
1860 tooltipManager->addTooltipFor(&m_remapPalButton, Strings::color_bar_remap_palette_tooltip(), BOTTOM);
1861 tooltipManager->addTooltipFor(&m_remapTilesButton, Strings::color_bar_remap_tiles_tooltip(), BOTTOM);
1862
1863 tooltipManager->addTooltipFor(
1864 m_tilesButton.getItem(0),
1865 key_tooltip(Strings::color_bar_switch_tileset().c_str(), CommandId::ToggleTilesMode()),
1866 BOTTOM);
1867 Command* cmd = Commands::instance()->byId(CommandId::TilesetMode());
1868 Params params;
1869 params.set("mode", "manual");
1870 tooltipManager->addTooltipFor(
1871 m_tilesetModeButtons.getItem((int)TilesetMode::Manual),
1872 key_tooltip(Strings::color_bar_tileset_mode_manual().c_str(), cmd->id().c_str(), params), BOTTOM);
1873 params.set("mode", "auto");
1874 tooltipManager->addTooltipFor(
1875 m_tilesetModeButtons.getItem((int)TilesetMode::Auto),
1876 key_tooltip(Strings::color_bar_tileset_mode_auto().c_str(), cmd->id().c_str(), params), BOTTOM);
1877 params.set("mode", "stack");
1878 tooltipManager->addTooltipFor(
1879 m_tilesetModeButtons.getItem((int)TilesetMode::Stack),
1880 key_tooltip(Strings::color_bar_tileset_mode_stack().c_str(), cmd->id().c_str(), params), BOTTOM);
1881}
1882
1883// static
1884void ColorBar::fixColorIndex(ColorButton& colorButton)
1885{
1886 app::Color color = colorButton.getColor();
1887
1888 if (color.getType() == Color::IndexType) {
1889 int oldIndex = color.getIndex();
1890 int newIndex = std::clamp(oldIndex, 0, get_current_palette()->size()-1);
1891 if (oldIndex != newIndex) {
1892 color = Color::fromIndex(newIndex);
1893 colorButton.setColor(color);
1894 }
1895 }
1896}
1897
1898void ColorBar::registerCommands()
1899{
1900 Commands::instance()
1901 ->add(
1902 new QuickCommand(
1903 CommandId::ShowPaletteSortOptions(),
1904 [this]{ this->showPaletteSortOptions(); }))
1905 ->add(
1906 new QuickCommand(
1907 CommandId::ShowPalettePresets(),
1908 [this]{ this->showPalettePresets(); }))
1909 ->add(
1910 new QuickCommand(
1911 CommandId::ShowPaletteOptions(),
1912 [this]{ this->showPaletteOptions(); }));
1913}
1914
1915void ColorBar::showPaletteSortOptions()
1916{
1917 gfx::Rect bounds = m_buttons.getItem(
1918 static_cast<int>(PalButton::SORT))->bounds();
1919
1920 Menu menu;
1921 MenuItem
1922 rev(Strings::color_bar_reverse_colors()),
1923 grd(Strings::color_bar_gradient()),
1924 grh(Strings::color_bar_gradient_by_hue()),
1925 hue(Strings::color_bar_sort_by_hue()),
1926 sat(Strings::color_bar_sort_by_saturation()),
1927 bri(Strings::color_bar_sort_by_brightness()),
1928 lum(Strings::color_bar_sort_by_luminance()),
1929 red(Strings::color_bar_sort_by_red()),
1930 grn(Strings::color_bar_sort_by_green()),
1931 blu(Strings::color_bar_sort_by_blue()),
1932 alp(Strings::color_bar_sort_by_alpha()),
1933 asc(Strings::color_bar_ascending()),
1934 des(Strings::color_bar_descending());
1935 menu.addChild(&rev);
1936 menu.addChild(&grd);
1937 menu.addChild(&grh);
1938 menu.addChild(new ui::MenuSeparator);
1939 menu.addChild(&hue);
1940 menu.addChild(&sat);
1941 menu.addChild(&bri);
1942 menu.addChild(&lum);
1943 menu.addChild(new ui::MenuSeparator);
1944 menu.addChild(&red);
1945 menu.addChild(&grn);
1946 menu.addChild(&blu);
1947 menu.addChild(&alp);
1948 menu.addChild(new ui::MenuSeparator);
1949 menu.addChild(&asc);
1950 menu.addChild(&des);
1951
1952 if (m_ascending) asc.setSelected(true);
1953 else des.setSelected(true);
1954
1955 rev.Click.connect([this]{ onReverseColors(); });
1956 grd.Click.connect([this]{ onGradient(GradientType::LINEAR); });
1957 grh.Click.connect([this]{ onGradient(GradientType::HUE); });
1958 hue.Click.connect([this]{ onSortBy(SortPaletteBy::HUE); });
1959 sat.Click.connect([this]{ onSortBy(SortPaletteBy::SATURATION); });
1960 bri.Click.connect([this]{ onSortBy(SortPaletteBy::VALUE); });
1961 lum.Click.connect([this]{ onSortBy(SortPaletteBy::LUMA); });
1962 red.Click.connect([this]{ onSortBy(SortPaletteBy::RED); });
1963 grn.Click.connect([this]{ onSortBy(SortPaletteBy::GREEN); });
1964 blu.Click.connect([this]{ onSortBy(SortPaletteBy::BLUE); });
1965 alp.Click.connect([this]{ onSortBy(SortPaletteBy::ALPHA); });
1966 asc.Click.connect([this]{ setAscending(true); });
1967 des.Click.connect([this]{ setAscending(false); });
1968
1969 menu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
1970}
1971
1972void ColorBar::showPalettePresets()
1973{
1974 if (!m_palettePopup) {
1975 try {
1976 m_palettePopup.reset(new PalettePopup());
1977 }
1978 catch (const std::exception& ex) {
1979 Console::showException(ex);
1980 return;
1981 }
1982 }
1983
1984 if (!m_palettePopup->isVisible()) {
1985 gfx::Rect bounds = m_buttons.getItem(
1986 static_cast<int>(PalButton::PRESETS))->bounds();
1987
1988 m_palettePopup->showPopup(display(), bounds);
1989 }
1990 else {
1991 m_palettePopup->closeWindow(nullptr);
1992 }
1993}
1994
1995void ColorBar::showPaletteOptions()
1996{
1997 Menu* menu = AppMenus::instance()->getPalettePopupMenu();
1998 if (menu) {
1999 gfx::Rect bounds = m_buttons.getItem(
2000 static_cast<int>(PalButton::OPTIONS))->bounds();
2001
2002 menu->showPopup(gfx::Point(bounds.x, bounds.y2()), display());
2003 }
2004}
2005
2006bool ColorBar::canEditTiles() const
2007{
2008 const Site site = UIContext::instance()->activeSite();
2009 return (site.layer() &&
2010 site.layer()->isTilemap());
2011}
2012
2013} // namespace app
2014