// 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

#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/bind.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/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::Manual) &&
                1 == int(TilesetMode::Auto) &&
                2 == int(TilesetMode::Stack), "Tileset mode buttons doesn't match TilesetMode enum values");

  m_tilesetModeButtons.addItem(theme->parts.tilesManual());
  m_tilesetModeButtons.addItem(theme->parts.tilesAuto());
  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_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);

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

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

  m_remapPalButton.Click.connect(base::Bind<void>(&ColorBar::onRemapPalButtonClick, this));
  m_remapTilesButton.Click.connect(base::Bind<void>(&ColorBar::onRemapTilesButtonClick, this));
  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(base::Bind<void>(&ColorBar::onFixWarningClick, this, &m_fgColor, m_fgWarningIcon));
  m_bgWarningIcon->Click.connect(base::Bind<void>(&ColorBar::onFixWarningClick, this, &m_bgColor, m_bgWarningIcon));
  m_redrawTimer.Tick.connect(base::Bind<void>(&ColorBar::onTimerTick, this));
  m_editPal.ItemChange.connect(base::Bind<void>(&ColorBar::onSwitchPalEditMode, this));
  m_buttons.ItemChange.connect(base::Bind<void>(&ColorBar::onPaletteButtonClick, this));
  m_tilesButton.ItemChange.connect(base::Bind<void>(&ColorBar::onTilesButtonClick, this));
  m_tilesetModeButtons.ItemChange.connect(base::Bind<void>(&ColorBar::onTilesetModeButtonClick, this));

  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(base::Bind<void>(&ColorBar::onFgColorChangeFromPreferences, this));
  m_bgConn = Preferences::instance().colorBar.bgColor.AfterChange.connect(base::Bind<void>(&ColorBar::onBgColorChangeFromPreferences, this));
  m_sepConn = Preferences::instance().colorBar.entriesSeparator.AfterChange.connect(base::Bind<void>(&ColorBar::invalidate, this));
  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(
    base::Bind<void>(&ColorBar::setupTooltips, this, 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_fgColor.getColor().getIndex();  // TODO
}

doc::tile_index ColorBar::getBgTile() const
{
  return m_bgColor.getColor().getIndex();  // TODO
}

PaletteView* ColorBar::getPaletteView()
{
  return &m_paletteView;
}

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(const TilemapMode mode)
{
  const Site site = UIContext::instance()->activeSite();
  const bool isTilemap = (site.layer() && site.layer()->isTilemap());
  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);
  }
  else {
    manager()->freeWidget(&m_tilesView);
    m_scrollablePalView.setVisible(true);
    m_scrollableTilesView.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);
  }
}

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;

    auto tileset = m_tilesView.tileset();

    // Remap all tiles in the same order as in newTileset
    Remap remap(tileset->size());
    for (tile_index ti=0; ti<remap.size(); ++ti) {
      auto img = m_oldTileset->get(ti);
      tile_index destTi = (img ? tileset->findTileIndex(img):
                                 doc::tile_i_notile);
      if (img && destTi != doc::tile_i_notile) {
        COLOR_BAR_TRACE(" - Remap tile %d -> %d\n", ti, destTi);
        remap.map(ti, destTi);
      }
      else {
        remap.map(ti, ti);
      }
    }
    // Nothing to remap
    if (remap.isIdentity()) {
      COLOR_BAR_TRACE(" - Nothing to remap\n");
      return;
    }

    Tx tx(writer.context(), Strings::color_bar_remap_tiles(), ModifyDocument);
    tx(new cmd::RemapTilemaps(tileset, remap));
    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)
{
  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 (doc::tile_index ti=0; ti<picks.size(); ++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;

  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 {
    ContextWriter writer(UIContext::instance(), 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());
  }
  catch (base::Exception& e) {
    Console::showException(e);
  }
}

void ColorBar::onTilesViewIndexChange(int index, ui::MouseButton button)
{
  // TODO show tools to stamp/draw/pick tiles

  if (button == kButtonRight)
    setBgColor(app::Color::fromIndex(index));
  else if (button == kButtonLeft)
    setFgColor(app::Color::fromIndex(index));
  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::onFgColorButtonBeforeChange(app::Color& color)
{
  COLOR_BAR_TRACE("ColorBar::onFgColorButtonBeforeChange(%s)\n", color.toString().c_str());

  if (m_fromPalView)
    return;

  if (!inEditMode()) {
    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() ||
      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()
{
  int index1, index2;
  if (!m_paletteView.getSelectedRange(index1, index2))
    return;

  Palette newPalette(*get_current_palette());
  newPalette.makeGradient(index1, index2);

  setPalette(&newPalette, "Gradient");
}

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)
{
  if (m_tilemapMode == TilemapMode::Tiles)
    return (clipboard::get_current_format() == clipboard::ClipboardTiles);
  else
    return (clipboard::get_current_format() == clipboard::ClipboardPaletteEntries);
}

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

bool ColorBar::onCut(Context* ctx)
{
  if (m_tilemapMode == TilemapMode::Tiles) {
    m_tilesView.cutToClipboard();
    showRemapTiles();
  }
  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) {
    m_tilesView.pasteFromClipboard();
    showRemapTiles();
  }
  else
    m_paletteView.pasteFromClipboard();
  return true;
}

bool ColorBar::onClear(Context* ctx)
{
  if (m_tilemapMode == TilemapMode::Tiles) {
    m_tilesView.clearSelection();
    showRemapTiles();
  }
  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"),
    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(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(base::Bind<void>(&ColorBar::onReverseColors, this));
  grd.Click.connect(base::Bind<void>(&ColorBar::onGradient, this));
  hue.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::HUE));
  sat.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::SATURATION));
  bri.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::VALUE));
  lum.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::LUMA));
  red.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::RED));
  grn.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::GREEN));
  blu.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::BLUE));
  alp.Click.connect(base::Bind<void>(&ColorBar::onSortBy, this, SortPaletteBy::ALPHA));
  asc.Click.connect(base::Bind<void>(&ColorBar::setAscending, this, true));
  des.Click.connect(base::Bind<void>(&ColorBar::setAscending, this, 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
