// Aseprite
// Copyright (C) 2018-2020  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

#define COLOR_BAR_TRACE(...) // TRACE(__VA_ARGS__)

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "app/ui/color_bar.h"

#include "app/app.h"
#include "app/app_menus.h"
#include "app/cmd/remap_colors.h"
#include "app/cmd/remap_tilemaps.h"
#include "app/cmd/remap_tileset.h"
#include "app/cmd/remove_tile.h"
#include "app/cmd/replace_image.h"
#include "app/cmd/set_palette.h"
#include "app/cmd/set_transparent_color.h"
#include "app/cmd_sequence.h"
#include "app/color.h"
#include "app/commands/command.h"
#include "app/commands/commands.h"
#include "app/commands/params.h"
#include "app/commands/quick_command.h"
#include "app/console.h"
#include "app/context_access.h"
#include "app/doc_api.h"
#include "app/doc_undo.h"
#include "app/i18n/strings.h"
#include "app/ini_file.h"
#include "app/modules/editors.h"
#include "app/modules/gui.h"
#include "app/modules/palettes.h"
#include "app/pref/preferences.h"
#include "app/tx.h"
#include "app/ui/color_spectrum.h"
#include "app/ui/color_tint_shade_tone.h"
#include "app/ui/color_wheel.h"
#include "app/ui/editor/editor.h"
#include "app/ui/hex_color_entry.h"
#include "app/ui/input_chain.h"
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui/palette_popup.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui_context.h"
#include "app/ui_context.h"
#include "app/util/cel_ops.h"
#include "app/util/clipboard.h"
#include "base/clamp.h"
#include "base/scoped_value.h"
#include "doc/cel.h"
#include "doc/cels_range.h"
#include "doc/image.h"
#include "doc/image_impl.h"
#include "doc/layer_tilemap.h"
#include "doc/palette.h"
#include "doc/palette_gradient_type.h"
#include "doc/primitives.h"
#include "doc/remap.h"
#include "doc/rgbmap.h"
#include "doc/sort_palette.h"
#include "doc/sprite.h"
#include "doc/tileset.h"
#include "os/surface.h"
#include "ui/alert.h"
#include "ui/graphics.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/paint_event.h"
#include "ui/separator.h"
#include "ui/splitter.h"
#include "ui/system.h"
#include "ui/tooltips.h"

#include <cstring>
#include <limits>

namespace app {

enum class PalButton {
  SORT,
  PRESETS,
  OPTIONS,
  MAX
};

using namespace app::skin;
using namespace ui;

class ColorBar::WarningIcon : public ui::Button {
public:
  WarningIcon() : ui::Button(std::string()) {
    initTheme();
  }
protected:
  void onInitTheme(ui::InitThemeEvent& ev) {
    ui::Button::onInitTheme(ev);
    setStyle(skin::SkinTheme::instance()->styles.warningBox());
  }
};

//////////////////////////////////////////////////////////////////////
// ColorBar::ScrollableView class

ColorBar::ScrollableView::ScrollableView()
{
  initTheme();
}

void ColorBar::ScrollableView::onInitTheme(InitThemeEvent& ev)
{
  auto hbar = horizontalBar();
  auto vbar = verticalBar();
  setup_mini_look(hbar);
  setup_mini_look(vbar);

  View::onInitTheme(ev);

  SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
  setStyle(theme->styles.colorbarView());

  hbar->setStyle(theme->styles.miniScrollbar());
  vbar->setStyle(theme->styles.miniScrollbar());
  hbar->setThumbStyle(theme->styles.miniScrollbarThumb());
  vbar->setThumbStyle(theme->styles.miniScrollbarThumb());

  const int scrollBarWidth = theme->dimensions.miniScrollbarSize();
  hbar->setBarWidth(scrollBarWidth);
  vbar->setBarWidth(scrollBarWidth);
}

//////////////////////////////////////////////////////////////////////
// ColorBar class

ColorBar* ColorBar::m_instance = NULL;

ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
  : Box(align)
  , m_editPal(1)
  , m_buttons(int(PalButton::MAX))
  , m_tilesButton(1)
  , m_tilesetModeButtons(3)
  , m_splitter(Splitter::ByPercentage, VERTICAL)
  , m_paletteView(true, PaletteView::FgBgColors, this, 16)
  , m_tilesView(true, PaletteView::FgBgTiles, this, 16)
  , m_remapPalButton(Strings::color_bar_remap_palette())
  , m_remapTilesButton(Strings::color_bar_remap_tiles())
  , m_selector(ColorSelector::NONE)
  , m_tintShadeTone(nullptr)
  , m_spectrum(nullptr)
  , m_wheel(nullptr)
  , m_fgColor(app::Color::fromRgb(255, 255, 255), IMAGE_RGB, ColorBarButtonsOptions())
  , m_bgColor(app::Color::fromRgb(0, 0, 0), IMAGE_RGB, ColorBarButtonsOptions())
  , m_fgWarningIcon(new WarningIcon)
  , m_bgWarningIcon(new WarningIcon)
  , m_fromPalView(false)
  , m_fromPref(false)
  , m_fromFgButton(false)
  , m_fromBgButton(false)
  , m_lastDocument(nullptr)
  , m_lastTilesetId(doc::NullId)
  , m_ascending(true)
  , m_lastButton(kButtonLeft)
  , m_editMode(false)
  , m_tilemapMode(TilemapMode::Pixels)
  , m_tilesetMode(TilesetMode::Auto)
  , m_redrawTimer(250, this)
  , m_redrawAll(false)
  , m_implantChange(false)
  , m_selfPalChange(false)
{
  m_instance = this;

  SkinTheme* theme = static_cast<SkinTheme*>(this->theme());

  m_editPal.addItem(theme->parts.timelineOpenPadlockActive());
  m_buttons.addItem(theme->parts.palSort());
  m_buttons.addItem(theme->parts.palPresets());
  m_buttons.addItem(theme->parts.palOptions());
  m_tilesButton.addItem(theme->parts.tiles());

  static_assert(0 == int(TilesetMode::Auto) &&
                1 == int(TilesetMode::Manual) &&
                2 == int(TilesetMode::Stack), "Tileset mode buttons doesn't match TilesetMode enum values");

  m_tilesetModeButtons.addItem(theme->parts.tilesAuto());
  m_tilesetModeButtons.addItem(theme->parts.tilesManual());
  m_tilesetModeButtons.addItem(theme->parts.tilesStack());
  setTilesetMode(m_tilesetMode);

  m_paletteView.setColumns(8);
  m_tilesView.setColumns(8);

  m_scrollablePalView.attachToView(&m_paletteView);
  m_scrollableTilesView.attachToView(&m_tilesView);
  m_scrollablePalView.setExpansive(true);
  m_scrollableTilesView.setExpansive(true);

  m_scrollableTilesView.setVisible(false);
  m_tilesHelpers.setVisible(false);
  m_remapPalButton.setVisible(false);
  m_remapTilesButton.setVisible(false);

  m_palettePlaceholder.addChild(&m_scrollablePalView);
  m_palettePlaceholder.addChild(&m_scrollableTilesView);
  m_palettePlaceholder.addChild(&m_remapPalButton);
  m_palettePlaceholder.addChild(&m_remapTilesButton);
  m_splitter.setId("palette_spectrum_splitter");
  m_splitter.setPosition(80);
  m_splitter.setExpansive(true);
  m_splitter.addChild(&m_palettePlaceholder);
  m_splitter.addChild(&m_selectorPlaceholder);

  setColorSelector(
    Preferences::instance().colorBar.selector());

  m_tilesHBox.addChild(&m_tilesButton);
  m_tilesHBox.addChild(&m_tilesetModeButtons);

  m_palHBox.addChild(&m_editPal);
  m_palHBox.addChild(&m_buttons);

  m_buttons.setExpansive(true);
  m_tilesetModeButtons.setExpansive(true);

  // Hide the tiles controls by default. Without this, when the first
  // onActiveSiteChange() event is received, and the we ask for the
  // m_tilesHBox visibility, it might say that it's hidden because the
  // color bar is hidden (because it's not yet in the screen, it's the
  // first time it will be displayed). So we have to add this to make
  // the tiles controls invisible in the first appearance of the color
  // bar.
  m_tilesHBox.setVisible(false);

  addChild(&m_palHBox);
  addChild(&m_tilesHBox);
  addChild(&m_splitter);

  addChild(&m_colorHelpers);
  addChild(&m_tilesHelpers);

  HBox* fgBox = new HBox;
  HBox* bgBox = new HBox;
  fgBox->addChild(&m_fgColor);
  fgBox->addChild(m_fgWarningIcon);
  bgBox->addChild(&m_bgColor);
  bgBox->addChild(m_bgWarningIcon);
  m_colorHelpers.addChild(fgBox);
  m_colorHelpers.addChild(bgBox);

  m_tilesHelpers.addChild(new BoxFiller);
  m_tilesHelpers.addChild(&m_fgTile);
  m_tilesHelpers.addChild(&m_bgTile);
  m_tilesHelpers.addChild(new BoxFiller);

  m_fgColor.setId("fg_color");
  m_bgColor.setId("bg_color");
  m_fgColor.setExpansive(true);
  m_bgColor.setExpansive(true);

  m_remapPalButton.Click.connect([this]{ onRemapPalButtonClick(); });
  m_remapTilesButton.Click.connect([this]{ onRemapTilesButtonClick(); });
  m_fgColor.Change.connect(&ColorBar::onFgColorButtonChange, this);
  m_fgColor.BeforeChange.connect(&ColorBar::onFgColorButtonBeforeChange, this);
  m_bgColor.Change.connect(&ColorBar::onBgColorButtonChange, this);
  m_fgWarningIcon->Click.connect([this]{ onFixWarningClick(&m_fgColor, m_fgWarningIcon); });
  m_bgWarningIcon->Click.connect([this]{ onFixWarningClick(&m_bgColor, m_bgWarningIcon); });
  m_redrawTimer.Tick.connect([this]{ onTimerTick(); });
  m_editPal.ItemChange.connect([this]{ onSwitchPalEditMode(); });
  m_buttons.ItemChange.connect([this]{ onPaletteButtonClick(); });
  m_tilesButton.ItemChange.connect([this]{ onTilesButtonClick(); });
  m_tilesetModeButtons.ItemChange.connect([this]{ onTilesetModeButtonClick(); });

  InitTheme.connect(
    [this, fgBox, bgBox]{
      SkinTheme* theme = static_cast<SkinTheme*>(this->theme());

      setBorder(gfx::Border(2*guiscale(), 0, 0, 0));
      setChildSpacing(2*guiscale());

      m_fgColor.resetSizeHint();
      m_bgColor.resetSizeHint();
      m_fgColor.setSizeHint(0, m_fgColor.sizeHint().h);
      m_bgColor.setSizeHint(0, m_bgColor.sizeHint().h);

      for (auto w : { &m_editPal, &m_buttons,
                      &m_tilesButton, &m_tilesetModeButtons }) {
        w->setMinSize(gfx::Size(0, theme->dimensions.colorBarButtonsHeight()));
        w->setMaxSize(gfx::Size(std::numeric_limits<int>::max(),
                                theme->dimensions.colorBarButtonsHeight())); // TODO add resetMaxSize
      }

      m_buttons.setMaxSize(
        gfx::Size(m_buttons.sizeHint().w,
                  theme->dimensions.colorBarButtonsHeight()));
      m_tilesetModeButtons.setMaxSize(
        gfx::Size(m_tilesetModeButtons.sizeHint().w,
                  theme->dimensions.colorBarButtonsHeight()));

      // Change color-bar background color (not ColorBar::setBgColor)
      this->Widget::setBgColor(theme->colors.tabActiveFace());
      m_paletteView.setBgColor(theme->colors.tabActiveFace());
      m_paletteView.setBoxSize(Preferences::instance().colorBar.boxSize());
      m_paletteView.initTheme();

      m_tilesView.setBgColor(theme->colors.tabActiveFace());
      m_tilesView.setBoxSize(Preferences::instance().colorBar.tilesBoxSize());
      m_tilesView.initTheme();

      // Styles
      m_splitter.setStyle(theme->styles.workspaceSplitter());

      fgBox->noBorderNoChildSpacing();
      bgBox->noBorderNoChildSpacing();

      if (m_palettePopup)
        m_palettePopup->initTheme();
    });
  initTheme();

  // Set background color reading its value from the configuration.
  setBgColor(Preferences::instance().colorBar.bgColor());

  // Clear the selection of the BG color in the palette.
  m_paletteView.deselect();

  // Set foreground color reading its value from the configuration.
  setFgColor(Preferences::instance().colorBar.fgColor());

  // Tooltips
  setupTooltips(tooltipManager);

  onColorButtonChange(getFgColor());

  UIContext::instance()->add_observer(this);
  m_beforeCmdConn = UIContext::instance()->BeforeCommandExecution.connect(&ColorBar::onBeforeExecuteCommand, this);
  m_afterCmdConn = UIContext::instance()->AfterCommandExecution.connect(&ColorBar::onAfterExecuteCommand, this);
  m_fgConn = Preferences::instance().colorBar.fgColor.AfterChange.connect([this]{ onFgColorChangeFromPreferences(); });
  m_bgConn = Preferences::instance().colorBar.bgColor.AfterChange.connect([this]{ onBgColorChangeFromPreferences(); });
  m_fgTileConn = Preferences::instance().colorBar.fgTile.AfterChange.connect([this]{ onFgTileChangeFromPreferences(); });
  m_bgTileConn = Preferences::instance().colorBar.bgTile.AfterChange.connect([this]{ onBgTileChangeFromPreferences(); });
  m_sepConn = Preferences::instance().colorBar.entriesSeparator.AfterChange.connect([this]{ invalidate(); });
  m_paletteView.FocusOrClick.connect(&ColorBar::onFocusPaletteOrTilesView, this);
  m_tilesView.FocusOrClick.connect(&ColorBar::onFocusPaletteOrTilesView, this);
  m_appPalChangeConn = App::instance()->PaletteChange.connect(&ColorBar::onAppPaletteChange, this);
  KeyboardShortcuts::instance()->UserChange.connect(
    [this, tooltipManager]{ setupTooltips(tooltipManager); });

  setEditMode(false);
  registerCommands();
}

ColorBar::~ColorBar()
{
  UIContext::instance()->remove_observer(this);
}

void ColorBar::setPixelFormat(PixelFormat pixelFormat)
{
  m_fgColor.setPixelFormat(pixelFormat);
  m_bgColor.setPixelFormat(pixelFormat);
}

app::Color ColorBar::getFgColor() const
{
  return m_fgColor.getColor();
}

app::Color ColorBar::getBgColor() const
{
  return m_bgColor.getColor();
}

void ColorBar::setFgColor(const app::Color& color)
{
  if (m_fromFgButton)
    return;

  m_fgColor.setColor(color);
  if (!m_fromPalView)
    onColorButtonChange(color);
}

void ColorBar::setBgColor(const app::Color& color)
{
  if (m_fromBgButton)
    return;

  m_bgColor.setColor(color);
  if (!m_fromPalView)
    onColorButtonChange(color);
}

doc::tile_index ColorBar::getFgTile() const
{
  return m_fgTile.getTile();
}

doc::tile_index ColorBar::getBgTile() const
{
  return m_bgTile.getTile();
}

ColorBar::ColorSelector ColorBar::getColorSelector() const
{
  return m_selector;
}

void ColorBar::setColorSelector(ColorSelector selector)
{
  if (m_selector == selector)
    return;

  if (m_tintShadeTone) m_tintShadeTone->setVisible(false);
  if (m_spectrum) m_spectrum->setVisible(false);
  if (m_wheel) m_wheel->setVisible(false);

  m_selector = selector;
  Preferences::instance().colorBar.selector(m_selector);

  switch (m_selector) {

    case ColorSelector::TINT_SHADE_TONE:
      if (!m_tintShadeTone) {
        m_tintShadeTone = new ColorTintShadeTone;
        m_tintShadeTone->setExpansive(true);
        m_tintShadeTone->selectColor(m_fgColor.getColor());
        m_tintShadeTone->ColorChange.connect(&ColorBar::onPickSpectrum, this);
        m_selectorPlaceholder.addChild(m_tintShadeTone);
      }
      m_tintShadeTone->setVisible(true);
      break;

    case ColorSelector::SPECTRUM:
      if (!m_spectrum) {
        m_spectrum = new ColorSpectrum;
        m_spectrum->setExpansive(true);
        m_spectrum->selectColor(m_fgColor.getColor());
        m_spectrum->ColorChange.connect(&ColorBar::onPickSpectrum, this);
        m_selectorPlaceholder.addChild(m_spectrum);
      }
      m_spectrum->setVisible(true);
      break;

    case ColorSelector::RGB_WHEEL:
    case ColorSelector::RYB_WHEEL:
    case ColorSelector::NORMAL_MAP_WHEEL:
      if (!m_wheel) {
        m_wheel = new ColorWheel;
        m_wheel->setExpansive(true);
        m_wheel->selectColor(m_fgColor.getColor());
        m_wheel->ColorChange.connect(&ColorBar::onPickSpectrum, this);
        m_selectorPlaceholder.addChild(m_wheel);
      }
      if (m_selector == ColorSelector::RGB_WHEEL) {
        m_wheel->setColorModel(ColorWheel::ColorModel::RGB);
      }
      else if (m_selector == ColorSelector::RYB_WHEEL) {
        m_wheel->setColorModel(ColorWheel::ColorModel::RYB);
      }
      else if (m_selector == ColorSelector::NORMAL_MAP_WHEEL) {
        m_wheel->setColorModel(ColorWheel::ColorModel::NORMAL_MAP);
      }
      m_wheel->setVisible(true);
      break;

  }

  m_selectorPlaceholder.layout();
}

bool ColorBar::inEditMode() const
{
  return
    (m_editMode &&
     m_lastDocument &&
     m_lastDocument->sprite() &&
     m_lastDocument->sprite()->pixelFormat() != IMAGE_GRAYSCALE);
}

void ColorBar::setEditMode(bool state)
{
  SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
  ButtonSet::Item* item = m_editPal.getItem(0);

  m_editMode = state;
  item->setIcon(state ? theme->parts.timelineOpenPadlockActive():
                        theme->parts.timelineClosedPadlockNormal());
  item->setHotColor(state ? theme->colors.editPalFace(): gfx::ColorNone);

  // Deselect color entries when we cancel editing
  if (!state)
    m_paletteView.deselect();
}

TilemapMode ColorBar::tilemapMode() const
{
  return
    (m_tilesHBox.isVisible() &&
     m_lastDocument &&
     m_lastDocument->sprite()) ? m_tilemapMode:
                                 TilemapMode::Pixels;
}

void ColorBar::setTilemapMode(TilemapMode mode)
{
  const Site site = UIContext::instance()->activeSite();
  const bool isTilemap = (site.layer() && site.layer()->isTilemap());
  if (!isTilemap)
    mode = TilemapMode::Pixels;

  const bool editTiles = (mode == TilemapMode::Tiles);

  SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
  ButtonSet::Item* item = m_tilesButton.getItem(0);

  m_tilemapMode = mode;
  item->setHotColor(editTiles ?
                    theme->colors.editPalFace():
                    gfx::ColorNone);
  item->setMono(true);

  if (editTiles && isTilemap) {
    manager()->freeWidget(&m_paletteView);
    m_scrollablePalView.setVisible(false);
    m_scrollableTilesView.setVisible(true);
    m_selectorPlaceholder.setVisible(false);
    m_colorHelpers.setVisible(false);
    m_tilesHelpers.setVisible(true);
  }
  else {
    manager()->freeWidget(&m_tilesView);
    m_scrollablePalView.setVisible(true);
    m_scrollableTilesView.setVisible(false);
    m_selectorPlaceholder.setVisible(true);
    m_colorHelpers.setVisible(true);
    m_tilesHelpers.setVisible(false);
  }

  layout();
}

TilesetMode ColorBar::tilesetMode() const
{
  if (m_tilesHBox.isVisible() &&
      m_lastDocument &&
      m_lastDocument->sprite()) {
    return m_tilesetMode;
  }
  else
    return TilesetMode::Manual;
}

void ColorBar::setTilesetMode(const TilesetMode mode)
{
  m_tilesetMode = mode;

  for (int i=0; i<3; ++i) {
    ButtonSet::Item* item = m_tilesetModeButtons.getItem(i);
    item->setSelected(int(mode) == i);
  }

  // Change to pixels mode automatically
  if (m_tilemapMode == TilemapMode::Tiles)
    setTilemapMode(TilemapMode::Pixels);
}

void ColorBar::onActiveSiteChange(const Site& site)
{
  if (m_lastDocument != site.document()) {
    if (m_lastDocument)
      m_lastDocument->remove_observer(this);

    m_lastDocument = const_cast<Doc*>(site.document());

    if (m_lastDocument)
      m_lastDocument->add_observer(this);

    hideRemapPal();
    hideRemapTiles();
  }

  bool isTilemap = false;
  if (site.layer())
    isTilemap = site.layer()->isTilemap();

  if (m_tilesHBox.isVisible() != isTilemap) {
    m_tilesHBox.setVisible(isTilemap);
    layout();
  }

  if (isTilemap) {
    doc::ObjectId newTilesetId =
      static_cast<const doc::LayerTilemap*>(site.layer())->tileset()->id();
    if (m_lastTilesetId != newTilesetId) {
      m_lastTilesetId = newTilesetId;
      m_scrollableTilesView.updateView();
    }
  }
  if (!isTilemap) {
    m_lastTilesetId = doc::NullId;
    if (m_tilemapMode == TilemapMode::Tiles)
      setTilemapMode(TilemapMode::Pixels);
  }
}

void ColorBar::onGeneralUpdate(DocEvent& ev)
{
  // TODO Observe palette changes only
  invalidate();
}

void ColorBar::onTilesetChanged(DocEvent& ev)
{
  // This can happen when a filter is applied to each tile in a
  // background thread.
  if (!ui::is_ui_thread())
    return;

  if (m_scrollableTilesView.isVisible())
    m_scrollableTilesView.updateView();

  m_tilesView.deselect();
}

void ColorBar::onAppPaletteChange()
{
  COLOR_BAR_TRACE("ColorBar::onAppPaletteChange()\n");

  fixColorIndex(m_fgColor);
  fixColorIndex(m_bgColor);

  updateWarningIcon(m_fgColor.getColor(), m_fgWarningIcon);
  updateWarningIcon(m_bgColor.getColor(), m_bgWarningIcon);
}

void ColorBar::onFocusPaletteOrTilesView(ui::Message* msg)
{
  App::instance()->inputChain().prioritize(this, msg);
}

void ColorBar::onBeforeExecuteCommand(CommandExecutionEvent& ev)
{
  if (ev.command()->id() == CommandId::SetPalette() ||
      ev.command()->id() == CommandId::LoadPalette() ||
      ev.command()->id() == CommandId::ColorQuantization()) {
    showRemapPal();
  }
}

void ColorBar::onAfterExecuteCommand(CommandExecutionEvent& ev)
{
  if (ev.command()->id() == CommandId::Undo() ||
      ev.command()->id() == CommandId::Redo())
    invalidate();

  // If the sprite isn't Indexed anymore (e.g. because we've just
  // undone a "RGB -> Indexed" conversion), we hide the "Remap
  // Palette" button.
  Site site = UIContext::instance()->activeSite();
  if (site.sprite() &&
      site.sprite()->pixelFormat() != IMAGE_INDEXED) {
    hideRemapPal();
  }

  // If the layer isn't a tilemap anymore, we hide the "Remap Tiles"
  // button.
  if (site.layer() &&
      !site.layer()->isTilemap()) {
    hideRemapTiles();
  }
}

void ColorBar::onSwitchPalEditMode()
{
  m_editPal.deselectItems();
  setEditMode(!inEditMode());
}

// Switches the palette-editor
void ColorBar::onPaletteButtonClick()
{
  int item = m_buttons.selectedItem();
  m_buttons.deselectItems();

  switch (static_cast<PalButton>(item)) {

    case PalButton::SORT:
      showPaletteSortOptions();
      break;

    case PalButton::PRESETS:
      showPalettePresets();
      break;

    case PalButton::OPTIONS:
      showPaletteOptions();
      break;

  }
}

void ColorBar::onTilesButtonClick()
{
  m_tilesButton.deselectItems();
  setTilemapMode(
    (tilemapMode() == TilemapMode::Pixels ? TilemapMode::Tiles:
                                            TilemapMode::Pixels));
}

void ColorBar::onTilesetModeButtonClick()
{
  int item = m_tilesetModeButtons.selectedItem();
  m_tilesetModeButtons.deselectItems();
  setTilesetMode(static_cast<TilesetMode>(item));
}

void ColorBar::onRemapPalButtonClick()
{
  ASSERT(m_oldPalette);

  // Create remap from m_oldPalette to the current palette
  Remap remap(1);
  try {
    ContextWriter writer(UIContext::instance());
    Sprite* sprite = writer.sprite();
    ASSERT(sprite);
    if (!sprite)
      return;

    remap = create_remap_to_change_palette(
      m_oldPalette.get(), get_current_palette(),
      sprite->transparentColor(), true);
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }

  // Check the remap
  if (!remap.isFor8bit() &&
      Alert::show(Strings::alerts_auto_remap()) != 1) {
    return;
  }

  try {
    ContextWriter writer(UIContext::instance());
    Sprite* sprite = writer.sprite();
    if (sprite) {
      ASSERT(sprite->pixelFormat() == IMAGE_INDEXED);

      Tx tx(writer.context(), "Remap Colors", ModifyDocument);
      bool remapPixels = true;

      std::vector<ImageRef> images;
      sprite->getImages(images);

      if (remap.isFor8bit()) {
        PalettePicks usedEntries(256);

        for (ImageRef& image : images) {
          for (const auto& i : LockImageBits<IndexedTraits>(image.get()))
            usedEntries[i] = true;
        }

        if (remap.isInvertible(usedEntries)) {
          tx(new cmd::RemapColors(sprite, remap));
          remapPixels = false;
        }
      }

      // Special remap saving original images in undo history
      if (remapPixels) {
        for (ImageRef& image : images) {
          ImageRef newImage(Image::createCopy(image.get()));
          doc::remap_image(newImage.get(), remap);

          tx(new cmd::ReplaceImage(sprite, image, newImage));
        }
      }

      color_t oldTransparent = sprite->transparentColor();
      color_t newTransparent = remap[oldTransparent];
      if (oldTransparent != newTransparent)
        tx(new cmd::SetTransparentColor(sprite, newTransparent));

      tx.commit();
    }
    update_screen_for_document(writer.document());
    hideRemapPal();
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::onRemapTilesButtonClick()
{
  COLOR_BAR_TRACE("remapTiles\n");
  try {
    ContextWriter writer(UIContext::instance(), 500);
    Sprite* sprite = writer.sprite();
    if (!sprite)
      return;

    doc::Tileset* tileset = m_tilesView.tileset();
    std::vector<ImageRef> tilemaps;
    sprite->getTilemapsByTileset(tileset, tilemaps);

    const int n = std::max(m_oldTileset->size(),
                           tileset->size());
    PalettePicks usedTiles(n);

    if (n > 0) {
      for (const ImageRef& tilemap : tilemaps) {
        for (const doc::tile_t t : LockImageBits<TilemapTraits>(tilemap.get()))
          if (t != doc::tile_i_notile)
            usedTiles[doc::tile_geti(t)] = true;
      }
    }

    // Remap all tiles in the same order as in newTileset
    Remap remap(n);
    bool existMapToEmpty = false;

    for (tile_index ti=0; ti<n; ++ti) {
      auto img = m_oldTileset->get(ti);
      tile_index destTi = (img ? tileset->findTileIndex(img):
                                 doc::tile_i_notile);
      if (img) {
        COLOR_BAR_TRACE(" - Remap tile %d -> %d\n", ti, destTi);
        remap.map(ti, destTi);
      }
      else {
        remap.map(ti, destTi = doc::tile_i_notile);
      }

      if (destTi == doc::tile_i_notile && ti < usedTiles.size() && usedTiles[ti]) {
        COLOR_BAR_TRACE(" - Remap tile %d to empty (used=%d)\n", ti, usedTiles[ti]);
        existMapToEmpty = true;
      }
    }

    // Nothing to remap
    if (remap.isIdentity()) {
      COLOR_BAR_TRACE(" - Nothing to remap\n");
      return;
    }

    Tx tx(writer.context(), Strings::color_bar_remap_tiles(), ModifyDocument);
    if (!existMapToEmpty &&
        remap.isInvertible(usedTiles)) {
      tx(new cmd::RemapTilemaps(tileset, remap));
    }
    else {
      for (const ImageRef& tilemap : tilemaps) {
        ImageRef newTilemap(Image::createCopy(tilemap.get()));
        doc::remap_image(newTilemap.get(), remap);

        // TODO improve this with a cmd::CopyRegion()
        tx(new cmd::ReplaceImage(sprite, tilemap, newTilemap));
      }
    }
    tx.commit();

    hideRemapTiles();
    // TODO this should be automatic in last ~Tx() destruction
    manager()->invalidate();
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::onPaletteViewIndexChange(int index, ui::MouseButton button)
{
  COLOR_BAR_TRACE("ColorBar::onPaletteViewIndexChange(%d)\n", index);

  base::ScopedValue<bool> lock(m_fromPalView, true, m_fromPalView);

  app::Color color = app::Color::fromIndex(index);

  if (button == kButtonRight)
    setBgColor(color);
  else if (button == kButtonLeft)
    setFgColor(color);
  else if (button == kButtonMiddle)
    setTransparentIndex(index);

  ChangeSelection();
}

void ColorBar::onPaletteViewModification(const Palette* newPalette,
                                         PaletteViewModification mod)
{
  const char* text = "Palette Change";
  switch (mod) {
    case PaletteViewModification::CLEAR: text = "Clear Colors"; break;
    case PaletteViewModification::DRAGANDDROP: text = "Drag-and-Drop Colors"; break;
    case PaletteViewModification::RESIZE: text = "Resize Palette"; break;
  }
  setPalette(newPalette, text);
}

void ColorBar::setPalette(const doc::Palette* newPalette, const std::string& actionText)
{
  showRemapPal();

  try {
    ContextWriter writer(UIContext::instance(), 500);
    Sprite* sprite = writer.sprite();
    frame_t frame = writer.frame();
    if (sprite &&
        newPalette->countDiff(sprite->palette(frame), nullptr, nullptr)) {
      Tx tx(writer.context(), actionText, ModifyDocument);
      tx(new cmd::SetPalette(sprite, frame, newPalette));
      tx.commit();
    }
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::setTransparentIndex(int index)
{
  try {
    ContextWriter writer(UIContext::instance());
    Sprite* sprite = writer.sprite();
    if (sprite &&
        sprite->pixelFormat() == IMAGE_INDEXED &&
        int(sprite->transparentColor()) != index) {
      // TODO merge this code with SpritePropertiesCommand
      Tx tx(writer.context(), "Set Transparent Color");
      DocApi api = writer.document()->getApi(tx);
      api.setSpriteTransparentColor(sprite, index);
      tx.commit();

      update_screen_for_document(writer.document());
    }
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::onPaletteViewChangeSize(int boxsize)
{
  if (tilemapMode() == TilemapMode::Tiles)
    Preferences::instance().colorBar.tilesBoxSize(boxsize);
  else
    Preferences::instance().colorBar.boxSize(boxsize);
}

void ColorBar::onPaletteViewPasteColors(
  const Palette* fromPal, const doc::PalettePicks& from, const doc::PalettePicks& _to)
{
  if (!from.picks() || !_to.picks()) // Nothing to do
    return;

  doc::PalettePicks to = _to;
  int to_first = to.firstPick();
  int to_last = to.lastPick();

  // Add extra picks in to range if it's needed to paste more colors.
  int from_picks = from.picks();
  int to_picks = to.picks();
  if (to_picks < from_picks) {
    for (int j=to_last+1; j<to.size() && to_picks<from_picks; ++j) {
      to[j] = true;
      ++to_picks;
    }
  }

  Palette newPalette(*get_current_palette());

  int i = 0;
  int j = to_first;

  for (auto state : from) {
    if (state) {
      if (j < newPalette.size())
        newPalette.setEntry(j, fromPal->getEntry(i));
      else
        newPalette.addEntry(fromPal->getEntry(i));

      for (++j; j<to.size(); ++j)
        if (to[j])
          break;
    }
    ++i;
  }

  setPalette(&newPalette, "Paste Colors");
}

app::Color ColorBar::onPaletteViewGetForegroundIndex()
{
  return getFgColor();
}

app::Color ColorBar::onPaletteViewGetBackgroundIndex()
{
  return getBgColor();
}

void ColorBar::onTilesViewClearTiles(const doc::PalettePicks& _picks)
{
  // Copy the collection of selected tiles because in case that the
  // user want to delete a range of tiles (several tiles at the same
  // time), after the first cmd::RemoveTile() is executed this
  // collection (_picks) will be modified and we'll lost the other
  // selected tiles to remove.
  doc::PalettePicks picks = _picks;
  try {
    ContextWriter writer(UIContext::instance(), 500);
    Sprite* sprite = writer.sprite();
    ASSERT(writer.layer()->isTilemap());
    if (sprite) {
      auto tileset = m_tilesView.tileset();

      Tx tx(writer.context(), "Clear Tiles", ModifyDocument);
      for (int ti=int(picks.size())-1; ti>=0; --ti) {
        if (picks[ti])
          tx(new cmd::RemoveTile(tileset, ti));
      }
      tx.commit();

      update_screen_for_document(writer.document());
    }
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::onTilesViewResize(const int newSize)
{
  auto tileset = m_tilesView.tileset();
  if (!tileset || tileset->size() == newSize)
    return;

  showRemapTiles();

  try {
    ContextWriter writer(UIContext::instance(), 500);
    Sprite* sprite = writer.sprite();
    ASSERT(writer.layer()->isTilemap());
    if (sprite) {
      auto tileset = m_tilesView.tileset();

      Tx tx(writer.context(), Strings::color_bar_resize_tiles(), ModifyDocument);
      if (tileset->size() < newSize) {
        for (doc::tile_index ti=tileset->size(); ti<newSize; ++ti) {
          ImageRef img = tileset->makeEmptyTile();
          tx(new cmd::AddTile(tileset, img));
        }
      }
      else {
        for (doc::tile_index ti=tileset->size()-1;
             ti!=(doc::tile_index)newSize-1; --ti) {
          tx(new cmd::RemoveTile(tileset, ti));
        }
      }

      tx.commit();

      // TODO this should be automatic (when tileset is changed after a transaction)
      m_scrollableTilesView.updateView();
      update_screen_for_document(writer.document());
    }
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::onTilesViewDragAndDrop(doc::Tileset* tileset,
                                      doc::PalettePicks& picks,
                                      int& currentEntry,
                                      const int beforeIndex,
                                      const bool isCopy)
{
  COLOR_BAR_TRACE("ColorBar::onTilesViewDragAndDrop() -> beforeIndex=%d\n",
                  beforeIndex);

  showRemapTiles();

  try {
    Context* ctx = UIContext::instance();
    ContextWriter writer(ctx, 500);
    Tx tx(writer.context(), Strings::color_bar_drag_and_drop_tiles(), ModifyDocument);
    if (isCopy)
      copy_tiles_in_tileset(tx, tileset, picks, currentEntry, beforeIndex);
    else
      move_tiles_in_tileset(tx, tileset, picks, currentEntry, beforeIndex);
    tx.commit();

    m_scrollableTilesView.updateView();
    update_screen_for_document(writer.document());

    ctx->setSelectedTiles(picks);
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::onTilesViewIndexChange(int index, ui::MouseButton button)
{
  auto& pref = Preferences::instance();
  if (button == kButtonRight)
    pref.colorBar.bgTile(doc::tile(index, 0));
  else if (button == kButtonLeft)
    pref.colorBar.fgTile(doc::tile(index, 0));
  else if (button == kButtonMiddle) {
    // TODO ?
  }
}

void ColorBar::onFgColorChangeFromPreferences()
{
  COLOR_BAR_TRACE("ColorBar::onFgColorChangeFromPreferences() -> %s\n",
                  Preferences::instance().colorBar.fgColor().toString().c_str());

  if (m_fromPref)
    return;

  base::ScopedValue<bool> sync(m_fromPref, true, false);
  setFgColor(Preferences::instance().colorBar.fgColor());
}

void ColorBar::onBgColorChangeFromPreferences()
{
  COLOR_BAR_TRACE("ColorBar::onBgColorChangeFromPreferences() -> %s\n",
                  Preferences::instance().colorBar.bgColor().toString().c_str());

  if (m_fromPref)
    return;

  if (inEditMode()) {
    // In edit mode, clicking with right-click will copy the color
    // selected with eyedropper to the active color entry.
    setFgColor(Preferences::instance().colorBar.bgColor());
  }
  else {
    base::ScopedValue<bool> sync(m_fromPref, true, false);
    setBgColor(Preferences::instance().colorBar.bgColor());
  }
}

void ColorBar::onFgTileChangeFromPreferences()
{
  if (m_fromPref)
    return;

  base::ScopedValue<bool> sync(m_fromPref, true, false);
  m_fgTile.setTile(Preferences::instance().colorBar.fgTile());
}

void ColorBar::onBgTileChangeFromPreferences()
{
  if (m_fromPref)
    return;

  base::ScopedValue<bool> sync(m_fromPref, true, false);
  m_bgTile.setTile(Preferences::instance().colorBar.bgTile());
}

void ColorBar::onFgColorButtonBeforeChange(app::Color& color)
{
  COLOR_BAR_TRACE("ColorBar::onFgColorButtonBeforeChange(%s)\n", color.toString().c_str());

  if (m_fromPalView)
    return;

  if (!inEditMode() || color.getType() == app::Color::IndexType) {
    m_paletteView.deselect();
    return;
  }

  // Here we change the selected colors in the
  // palette. "m_fromPref" must be false to edit the color. (It
  // means, if the eyedropper was used with the left-click, we don't
  // edit the color, we just select the color to as the normal
  // non-edit mode.)
  if (!m_fromPref) {
    int i = setPaletteEntry(color);
    if (i >= 0) {
      updateCurrentSpritePalette("Color Change");
      color = app::Color::fromIndex(i);
    }
  }
}

void ColorBar::onFgColorButtonChange(const app::Color& color)
{
  COLOR_BAR_TRACE("ColorBar::onFgColorButtonChange(%s)\n", color.toString().c_str());

  if (m_fromFgButton)
    return;

  base::ScopedValue<bool> lock(m_fromFgButton, true, false);

  if (!m_fromPref) {
    base::ScopedValue<bool> sync(m_fromPref, true, false);
    Preferences::instance().colorBar.fgColor(color);
  }

  updateWarningIcon(color, m_fgWarningIcon);
  onColorButtonChange(color);
}

void ColorBar::onBgColorButtonChange(const app::Color& color)
{
  COLOR_BAR_TRACE("ColorBar::onBgColorButtonChange(%s)\n", color.toString().c_str());

  if (m_fromBgButton)
    return;

  base::ScopedValue<bool> lock(m_fromBgButton, true, false);

  if (!m_fromPalView && !inEditMode())
    m_paletteView.deselect();

  if (!m_fromPref) {
    base::ScopedValue<bool> sync(m_fromPref, true, false);
    Preferences::instance().colorBar.bgColor(color);
  }

  updateWarningIcon(color, m_bgWarningIcon);
  onColorButtonChange(color);
}

void ColorBar::onColorButtonChange(const app::Color& color)
{
  COLOR_BAR_TRACE("ColorBar::onColorButtonChange(%s)\n", color.toString().c_str());

  if (!inEditMode() || color.getType() == app::Color::IndexType || m_fromPref) {
    if (color.getType() == app::Color::IndexType)
      m_paletteView.selectColor(color.getIndex());
    else {
      m_paletteView.selectExactMatchColor(color);

      // As foreground or background color changed, we've to redraw the
      // palette view fg/bg indicators.
      m_paletteView.invalidate();
    }
  }

  if (m_tintShadeTone && m_tintShadeTone->isVisible())
    m_tintShadeTone->selectColor(color);

  if (m_spectrum && m_spectrum->isVisible())
    m_spectrum->selectColor(color);

  if (m_wheel && m_wheel->isVisible())
    m_wheel->selectColor(color);
}

void ColorBar::onPickSpectrum(const app::Color& color, ui::MouseButton button)
{
  if (button == kButtonNone)
    button = m_lastButton;

  if (button == kButtonRight)
    setBgColor(color);
  else if (button == kButtonLeft)
    setFgColor(color);

  m_lastButton = button;
}

void ColorBar::onReverseColors()
{
  doc::PalettePicks entries;
  m_paletteView.getSelectedEntries(entries);

  entries.pickAllIfNeeded();
  int n = entries.picks();

  std::vector<int> mapToOriginal(n); // Maps index from selectedPalette -> palette
  int i = 0, j = 0;
  for (bool state : entries) {
    if (state)
      mapToOriginal[j++] = i;
    ++i;
  }

  Remap remap(get_current_palette()->size());
  i = 0;
  j = n;
  for (bool state : entries) {
    if (state)
      remap.map(i, mapToOriginal[--j]);
    else
      remap.map(i, i);
    ++i;
  }

  Palette newPalette(*get_current_palette(), remap);
  setPalette(&newPalette, "Reverse Colors");
}

void ColorBar::onSortBy(SortPaletteBy channel)
{
  PalettePicks entries;
  m_paletteView.getSelectedEntries(entries);

  entries.pickAllIfNeeded();
  int n = entries.picks();

  // Create a "subpalette" with selected entries only.
  Palette palette(*get_current_palette());
  Palette selectedPalette(0, n);
  std::vector<int> mapToOriginal(n); // Maps index from selectedPalette -> palette
  int i = 0, j = 0;
  for (bool state : entries) {
    if (state) {
      selectedPalette.setEntry(j, palette.getEntry(i));
      mapToOriginal[j] = i;
      ++j;
    }
    ++i;
  }

  // Create a remap to sort the selected entries with the given color
  // component/channel.
  Remap remap = doc::sort_palette(&selectedPalette, channel, m_ascending);

  // Create a bigger new remap for the original palette (with all
  // entries, selected and deselected).
  Remap remapOrig(palette.size());
  i = j = 0;
  for (bool state : entries) {
    if (state)
      remapOrig.map(i, mapToOriginal[remap[j++]]);
    else
      remapOrig.map(i, i);
    ++i;
  }

  // Create a new palette and apply the remap. This is the final new
  // palette for the sprite.
  Palette newPalette(palette, remapOrig);
  setPalette(&newPalette, "Sort Colors");
}

void ColorBar::onGradient(GradientType gradientType)
{
  int index1, index2;
  if (!m_paletteView.getSelectedRange(index1, index2))
    return;

  Palette newPalette(*get_current_palette());
  if (gradientType == GradientType::LINEAR) {
    newPalette.makeGradient(index1, index2);
    setPalette(&newPalette, "Gradient");
  }
  else {
    newPalette.makeHueGradient(index1, index2);
    setPalette(&newPalette, "Gradient by Hue");
  }
}

void ColorBar::setAscending(bool ascending)
{
  m_ascending = ascending;
}

void ColorBar::showRemapPal()
{
  Site site = UIContext::instance()->activeSite();
  if (site.sprite() &&
      site.sprite()->pixelFormat() == IMAGE_INDEXED) {
    if (!m_oldPalette) {
      m_oldPalette.reset(new Palette(*get_current_palette()));
      m_remapPalButton.setVisible(true);
      layout();
    }
  }
}

void ColorBar::showRemapTiles()
{
  Site site = UIContext::instance()->activeSite();
  if (site.layer() &&
      site.layer()->isTilemap()) {
    if (!m_oldTileset) {
      m_oldTileset.reset(
        Tileset::MakeCopyCopyingImages(
          static_cast<LayerTilemap*>(site.layer())->tileset()));
      m_remapTilesButton.setVisible(true);
      layout();
    }
  }
}

void ColorBar::hideRemapPal()
{
  if (!m_oldPalette)
    return;

  m_oldPalette.reset();
  m_remapPalButton.setVisible(false);
  layout();
}

void ColorBar::hideRemapTiles()
{
  if (!m_oldTileset)
    return;

  m_oldTileset.reset();
  m_remapTilesButton.setVisible(false);
  layout();
}

void ColorBar::onNewInputPriority(InputChainElement* element,
                                  const ui::Message* msg)
{
  if (dynamic_cast<Timeline*>(element) &&
      msg && (msg->ctrlPressed() || msg->shiftPressed()))
    return;

  if (element != this) {
    if (m_tilemapMode == TilemapMode::Tiles)
      m_tilesView.deselect();
    else
      m_paletteView.deselect();
  }
}

bool ColorBar::onCanCut(Context* ctx)
{
  if (m_tilemapMode == TilemapMode::Tiles)
    return (m_tilesView.getSelectedEntriesCount() > 0);
  else
    return (m_paletteView.getSelectedEntriesCount() > 0);
}

bool ColorBar::onCanCopy(Context* ctx)
{
  return onCanCut(ctx);
}

bool ColorBar::onCanPaste(Context* ctx)
{
  auto format = ctx->clipboard()->format();
  if (m_tilemapMode == TilemapMode::Tiles)
    return (format == ClipboardFormat::Tileset);
  else
    return (format == ClipboardFormat::PaletteEntries);
}

bool ColorBar::onCanClear(Context* ctx)
{
  return onCanCut(ctx);
}

bool ColorBar::onCut(Context* ctx)
{
  if (m_tilemapMode == TilemapMode::Tiles) {
    showRemapTiles();
    m_tilesView.cutToClipboard();
  }
  else
    m_paletteView.cutToClipboard();
  return true;
}

bool ColorBar::onCopy(Context* ctx)
{
  if (m_tilemapMode == TilemapMode::Tiles)
    m_tilesView.copyToClipboard();
  else
    m_paletteView.copyToClipboard();
  return true;
}

bool ColorBar::onPaste(Context* ctx)
{
  if (m_tilemapMode == TilemapMode::Tiles) {
    showRemapTiles();
    m_tilesView.pasteFromClipboard();
  }
  else
    m_paletteView.pasteFromClipboard();
  return true;
}

bool ColorBar::onClear(Context* ctx)
{
  if (m_tilemapMode == TilemapMode::Tiles) {
    showRemapTiles();
    m_tilesView.clearSelection();
  }
  else
    m_paletteView.clearSelection();
  return true;
}

void ColorBar::onCancel(Context* ctx)
{
  m_tilesView.deselect();
  m_tilesView.discardClipboardSelection();
  m_paletteView.deselect();
  m_paletteView.discardClipboardSelection();
  invalidate();
}

void ColorBar::onFixWarningClick(ColorButton* colorButton, ui::Button* warningIcon)
{
  COLOR_BAR_TRACE("ColorBar::onFixWarningClick(%s)\n", colorButton->getColor().toString().c_str());

  Palette* palette = get_current_palette();
  const int oldEntries = palette->size();

  Command* command = Commands::instance()->byId(CommandId::AddColor());
  Params params;
  params.set("source", "color");
  params.set("color", colorButton->getColor().toString().c_str());
  UIContext::instance()->executeCommand(command, params);

  // Select the new FG/BG color as an indexed color
  if (inEditMode()) {
    const int newEntries = palette->size();
    if (oldEntries != newEntries) {
      base::ScopedValue<bool> sync(m_fromPref, true, m_fromPref);
      app::Color newIndex = app::Color::fromIndex(newEntries-1);
      if (colorButton == &m_bgColor)
        setBgColor(newIndex);
      setFgColor(newIndex);
    }
  }
}

void ColorBar::onTimerTick()
{
  // Redraw all editors
  if (m_redrawAll) {
    m_redrawAll = false;
    m_implantChange = false;
    m_redrawTimer.stop();

    // Call all observers of PaletteChange event.
    m_selfPalChange = true;
    App::instance()->PaletteChange();
    m_selfPalChange = false;

    // Redraw all editors
    try {
      ContextWriter writer(UIContext::instance());
      Doc* document(writer.document());
      if (document != NULL)
        document->notifyGeneralUpdate();
    }
    catch (...) {
      // Do nothing
    }
  }
  // Redraw just the current editor
  else {
    m_redrawAll = true;
    if (current_editor)
      current_editor->updateEditor(true);
  }
}

void ColorBar::updateWarningIcon(const app::Color& color, ui::Button* warningIcon)
{
  int index = -1;

  if (color.getType() == app::Color::MaskType) {
    if (current_editor &&
        current_editor->sprite()) {
      index = current_editor->sprite()->transparentColor();
    }
    else
      index = 0;
  }
  else {
    index = get_current_palette()->findExactMatch(
      color.getRed(),
      color.getGreen(),
      color.getBlue(),
      color.getAlpha(), -1);
  }

  warningIcon->setVisible(index < 0);
  warningIcon->parent()->layout();
}

// Changes the selected color palettes with the given
// app::Color. Returns the first modified index in the palette.
int ColorBar::setPaletteEntry(const app::Color& color)
{
  int selIdx = m_paletteView.getSelectedEntry();
  if (selIdx < 0) {
    if (getFgColor().getType() == app::Color::IndexType) {
      selIdx = getFgColor().getIndex();
    }
  }

  PalettePicks entries;
  m_paletteView.getSelectedEntries(entries);
  if (entries.picks() == 0) {
    if (selIdx >= 0 && selIdx < entries.size()) {
      entries[selIdx] = true;
    }
  }

  doc::color_t c =
    doc::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());

  Palette* palette = get_current_palette();
  for (int i=0; i<palette->size(); ++i) {
    if (entries[i])
      palette->setEntry(i, c);
  }

  if (selIdx < 0 ||
      selIdx >= entries.size() ||
      !entries[selIdx])
    selIdx = entries.firstPick();

  return selIdx;
}

void ColorBar::updateCurrentSpritePalette(const char* operationName)
{
  if (UIContext::instance()->activeDocument() &&
      UIContext::instance()->activeDocument()->sprite()) {
    try {
      ContextWriter writer(UIContext::instance());
      Doc* document(writer.document());
      Sprite* sprite(writer.sprite());
      Palette* newPalette = get_current_palette(); // System current pal
      frame_t frame = writer.frame();
      Palette* currentSpritePalette = sprite->palette(frame); // Sprite current pal
      int from, to;

      // Check differences between current sprite palette and current system palette
      from = to = -1;
      currentSpritePalette->countDiff(newPalette, &from, &to);

      if (from >= 0 && to >= from) {
        DocUndo* undo = document->undoHistory();
        Cmd* cmd = new cmd::SetPalette(sprite, frame, newPalette);

        // Add undo information to save the range of pal entries that will be modified.
        if (m_implantChange &&
            undo->lastExecutedCmd() &&
            undo->lastExecutedCmd()->label() == operationName) {
          // Implant the cmd in the last CmdSequence if it's
          // related about color palette modifications
          ASSERT(dynamic_cast<CmdSequence*>(undo->lastExecutedCmd()));
          static_cast<CmdSequence*>(undo->lastExecutedCmd())->add(cmd);
          cmd->execute(UIContext::instance());
        }
        else {
          Tx tx(writer.context(), operationName, ModifyDocument);
          tx(cmd);
          tx.commit();
        }
      }
    }
    catch (base::Exception& e) {
      Console::showException(e);
    }
  }

  m_paletteView.invalidate();

  if (!m_redrawTimer.isRunning())
    m_redrawTimer.start();

  m_redrawAll = false;
  m_implantChange = true;
}

void ColorBar::setupTooltips(TooltipManager* tooltipManager)
{
  tooltipManager->addTooltipFor(&m_fgColor, Strings::color_bar_fg(), LEFT);
  tooltipManager->addTooltipFor(&m_bgColor, Strings::color_bar_bg(), LEFT);
  tooltipManager->addTooltipFor(m_fgWarningIcon, Strings::color_bar_fg_warning(), LEFT);
  tooltipManager->addTooltipFor(m_bgWarningIcon, Strings::color_bar_bg_warning(), LEFT);

  tooltipManager->addTooltipFor(
    m_editPal.getItem(0),
    key_tooltip(Strings::color_bar_edit_color().c_str(), CommandId::PaletteEditor()),
    BOTTOM);

  tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::SORT), Strings::color_bar_sort_and_gradients(), BOTTOM);
  tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::PRESETS), Strings::color_bar_presets(), BOTTOM);
  tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::OPTIONS), Strings::color_bar_options(), BOTTOM);
  tooltipManager->addTooltipFor(&m_remapPalButton, Strings::color_bar_remap_palette_tooltip(), BOTTOM);
  tooltipManager->addTooltipFor(&m_remapTilesButton, Strings::color_bar_remap_tiles_tooltip(), BOTTOM);

  tooltipManager->addTooltipFor(
    m_tilesButton.getItem(0),
    key_tooltip(Strings::color_bar_switch_tileset().c_str(), CommandId::ToggleTilesMode()),
    BOTTOM);
  tooltipManager->addTooltipFor(
    m_tilesetModeButtons.getItem((int)TilesetMode::Manual),
    Strings::color_bar_tileset_mode_manual(), BOTTOM);
  tooltipManager->addTooltipFor(
    m_tilesetModeButtons.getItem((int)TilesetMode::Auto),
    Strings::color_bar_tileset_mode_auto(), BOTTOM);
  tooltipManager->addTooltipFor(
    m_tilesetModeButtons.getItem((int)TilesetMode::Stack),
    Strings::color_bar_tileset_mode_stack(), BOTTOM);
}

// static
void ColorBar::fixColorIndex(ColorButton& colorButton)
{
  app::Color color = colorButton.getColor();

  if (color.getType() == Color::IndexType) {
    int oldIndex = color.getIndex();
    int newIndex = base::clamp(oldIndex, 0, get_current_palette()->size()-1);
    if (oldIndex != newIndex) {
      color = Color::fromIndex(newIndex);
      colorButton.setColor(color);
    }
  }
}

void ColorBar::registerCommands()
{
  Commands::instance()
    ->add(
      new QuickCommand(
        CommandId::ShowPaletteSortOptions(),
        [this]{ this->showPaletteSortOptions(); }))
    ->add(
      new QuickCommand(
        CommandId::ShowPalettePresets(),
        [this]{ this->showPalettePresets(); }))
    ->add(
      new QuickCommand(
        CommandId::ShowPaletteOptions(),
        [this]{ this->showPaletteOptions(); }));
}

void ColorBar::showPaletteSortOptions()
{
  gfx::Rect bounds = m_buttons.getItem(
    static_cast<int>(PalButton::SORT))->bounds();

  Menu menu;
  MenuItem
    rev("Reverse Colors"),
    grd("Gradient"),
    grh("Gradient by Hue"),
    hue("Sort by Hue"),
    sat("Sort by Saturation"),
    bri("Sort by Brightness"),
    lum("Sort by Luminance"),
    red("Sort by Red"),
    grn("Sort by Green"),
    blu("Sort by Blue"),
    alp("Sort by Alpha"),
    asc("Ascending"),
    des("Descending");
  menu.addChild(&rev);
  menu.addChild(&grd);
  menu.addChild(&grh);
  menu.addChild(new ui::MenuSeparator);
  menu.addChild(&hue);
  menu.addChild(&sat);
  menu.addChild(&bri);
  menu.addChild(&lum);
  menu.addChild(new ui::MenuSeparator);
  menu.addChild(&red);
  menu.addChild(&grn);
  menu.addChild(&blu);
  menu.addChild(&alp);
  menu.addChild(new ui::MenuSeparator);
  menu.addChild(&asc);
  menu.addChild(&des);

  if (m_ascending) asc.setSelected(true);
  else des.setSelected(true);

  rev.Click.connect([this]{ onReverseColors(); });
  grd.Click.connect([this]{ onGradient(GradientType::LINEAR); });
  grh.Click.connect([this]{ onGradient(GradientType::HUE); });
  hue.Click.connect([this]{ onSortBy(SortPaletteBy::HUE); });
  sat.Click.connect([this]{ onSortBy(SortPaletteBy::SATURATION); });
  bri.Click.connect([this]{ onSortBy(SortPaletteBy::VALUE); });
  lum.Click.connect([this]{ onSortBy(SortPaletteBy::LUMA); });
  red.Click.connect([this]{ onSortBy(SortPaletteBy::RED); });
  grn.Click.connect([this]{ onSortBy(SortPaletteBy::GREEN); });
  blu.Click.connect([this]{ onSortBy(SortPaletteBy::BLUE); });
  alp.Click.connect([this]{ onSortBy(SortPaletteBy::ALPHA); });
  asc.Click.connect([this]{ setAscending(true); });
  des.Click.connect([this]{ setAscending(false); });

  menu.showPopup(gfx::Point(bounds.x, bounds.y+bounds.h));
}

void ColorBar::showPalettePresets()
{
  if (!m_palettePopup) {
    try {
      m_palettePopup.reset(new PalettePopup());
    }
    catch (const std::exception& ex) {
      Console::showException(ex);
      return;
    }
  }

  if (!m_palettePopup->isVisible()) {
    gfx::Rect bounds = m_buttons.getItem(
      static_cast<int>(PalButton::PRESETS))->bounds();

    m_palettePopup->showPopup(
      gfx::Rect(bounds.x, bounds.y+bounds.h,
                ui::display_w()/2, ui::display_h()*3/4));
  }
  else {
    m_palettePopup->closeWindow(NULL);
  }
}

void ColorBar::showPaletteOptions()
{
  Menu* menu = AppMenus::instance()->getPalettePopupMenu();
  if (menu) {
    gfx::Rect bounds = m_buttons.getItem(
      static_cast<int>(PalButton::OPTIONS))->bounds();

    menu->showPopup(gfx::Point(bounds.x, bounds.y+bounds.h));
  }
}

} // namespace app
