Blob Blame Raw
#include "toonzqt/hexcolornames.h"

// TnzLib includes
#include "toonz/toonzfolders.h"

// TnzCore includes
#include "tconvert.h"
#include "tfiletype.h"
#include "tsystem.h"
#include "tcolorstyles.h"
#include "tpalette.h"
#include "tpixel.h"
#include "tvectorimage.h"
#include "trasterimage.h"
#include "tlevel_io.h"

// Qt includes
#include <QCoreApplication>
#include <QPainter>
#include <QMouseEvent>
#include <QLabel>
#include <QToolTip>
#include <QTabWidget>
#include <QCheckBox>
#include <QFileDialog>
#include <QMessageBox>

using namespace DVGui;

//-----------------------------------------------------------------------------

#define COLORNAMES_FILE "colornames.txt"

QMap<QString, QString> HexColorNames::s_maincolornames;
QMap<QString, QString> HexColorNames::s_usercolornames;
QMap<QString, QString> HexColorNames::s_tempcolornames;

HexColorNames *HexColorNames::instance() {
  static HexColorNames _instance;
  return &_instance;
}

HexColorNames::HexColorNames() {}

void HexColorNames::loadColorTableXML(QMap<QString, QString> &table,
                                    const TFilePath &fp) {
  if (!TFileStatus(fp).doesExist()) throw TException("File not found");

  TIStream is(fp);
  if (!is) throw TException("Can't read color names");

  std::string tagName;
  if (!is.matchTag(tagName) || tagName != "colors")
    throw TException("Not a color names file");

  while (!is.matchEndTag()) {
    if (!is.matchTag(tagName)) throw TException("Expected tag");
    if (tagName == "color") {
      QString name, hex;
      name = QString::fromStdString(is.getTagAttribute("name"));
      std::string hexs;
      is >> hexs;
      hex = QString::fromStdString(hexs);
      if (name.size() != 0 && hex.size() != 0)
        table.insert(name.toLower(), hex);
      if (!is.matchEndTag()) throw TException("Expected end tag");
    } else
      throw TException("unexpected tag /" + tagName + "/");
  }
}

//-----------------------------------------------------------------------------

void HexColorNames::saveColorTableXML(QMap<QString, QString> &table,
                                      const TFilePath &fp) {
  TOStream os(fp);
  if (!os) throw TException("Can't write color names");

  os.openChild("colors");

  QMap<QString, QString>::const_iterator it;
  std::map<std::string, std::string> attrs;
  for (it = table.cbegin(); it != table.cend(); ++it) {
    std::string nameStd = it.key().toStdString();
    attrs.clear();
    attrs.insert({"name", nameStd});
    os.openChild("color", attrs);
    os << it.value();
    os.closeChild();
  }

  os.closeChild();
}

//-----------------------------------------------------------------------------

bool HexColorNames::parseHexInternal(QString text, TPixel &outPixel) {
  bool ok;
  uint parsedValue = text.toUInt(&ok, 16);
  if (!ok) return false;

  switch (text.length()) {
  case 8:  // #RRGGBBAA
    outPixel.r = parsedValue >> 24;
    outPixel.g = parsedValue >> 16;
    outPixel.b = parsedValue >> 8;
    outPixel.m = parsedValue;
    break;
  case 6:  // #RRGGBB
    outPixel.r = parsedValue >> 16;
    outPixel.g = parsedValue >> 8;
    outPixel.b = parsedValue;
    outPixel.m = 255;
    break;
  case 4:  // #RGBA
    outPixel.r = (parsedValue >> 12) & 15;
    outPixel.r |= outPixel.r << 4;
    outPixel.g = (parsedValue >> 8) & 15;
    outPixel.g |= outPixel.g << 4;
    outPixel.b = (parsedValue >> 4) & 15;
    outPixel.b |= outPixel.b << 4;
    outPixel.m = parsedValue & 15;
    outPixel.m |= outPixel.m << 4;
    break;
  case 3:  // #RGB
    outPixel.r = (parsedValue >> 8) & 15;
    outPixel.r |= outPixel.r << 4;
    outPixel.g = (parsedValue >> 4) & 15;
    outPixel.g |= outPixel.g << 4;
    outPixel.b = parsedValue & 15;
    outPixel.b |= outPixel.b << 4;
    outPixel.m = 255;
    break;
  case 2:  // #VV (non-standard)
    outPixel.r = parsedValue;
    outPixel.g = outPixel.r;
    outPixel.b = outPixel.r;
    outPixel.m = 255;
    break;
  case 1:  // #V (non-standard)
    outPixel.r = parsedValue & 15;
    outPixel.r |= outPixel.r << 4;
    outPixel.g = outPixel.r;
    outPixel.b = outPixel.r;
    outPixel.m = 255;
    break;
  default:
    return false;
  }
  return true;
}

//-----------------------------------------------------------------------------

bool HexColorNames::loadMainFile(bool reload) {
  TFilePath mainCTFp = TEnv::getConfigDir() + COLORNAMES_FILE;

  // Load main color names
  try {
    if (reload || s_maincolornames.size() == 0) {
      s_maincolornames.clear();
      loadColorTableXML(s_maincolornames, mainCTFp);
    }
  } catch (...) {
    return false;
  }
  return true;
}

//-----------------------------------------------------------------------------

bool HexColorNames::hasUserFile() {
  TFilePath userCTFp = ToonzFolder::getMyModuleDir() + COLORNAMES_FILE;
  return TFileStatus(userCTFp).doesExist();
}

//-----------------------------------------------------------------------------

bool HexColorNames::loadUserFile(bool reload) {
  TFilePath userCTFp = ToonzFolder::getMyModuleDir() + COLORNAMES_FILE;

  // Load user color names (if exists...)
  if (TFileStatus(userCTFp).doesExist()) {
    try {
      if (reload || s_usercolornames.size() == 0) {
        s_usercolornames.clear();
        loadColorTableXML(s_usercolornames, userCTFp);
      }
    } catch (...) {
      return false;
    }
  }
  return true;
}

//-----------------------------------------------------------------------------

bool HexColorNames::loadTempFile(const TFilePath &fp) {
  if (TFileStatus(fp).doesExist()) {
    try {
      s_tempcolornames.clear();
      loadColorTableXML(s_tempcolornames, fp);
    } catch (...) {
      return false;
    }
  }
  return true;
}

//-----------------------------------------------------------------------------

bool HexColorNames::saveUserFile() {
  TFilePath userCTFp = ToonzFolder::getMyModuleDir() + COLORNAMES_FILE;

  try {
    saveColorTableXML(s_usercolornames, userCTFp);
  } catch (...) {
    return false;
  }
  return true;
}

//-----------------------------------------------------------------------------

bool HexColorNames::saveTempFile(const TFilePath &fp) {
  try {
    saveColorTableXML(s_tempcolornames, fp);
  } catch (...) {
    return false;
  }
  return true;
}

//-----------------------------------------------------------------------------

void HexColorNames::clearUserEntries() { s_usercolornames.clear(); }

void HexColorNames::clearTempEntries() { s_tempcolornames.clear(); }

//-----------------------------------------------------------------------------

void HexColorNames::setUserEntry(const QString &name, const QString &hex) {
  s_usercolornames.insert(name, hex);
}

//-----------------------------------------------------------------------------

void HexColorNames::setTempEntry(const QString &name, const QString &hex) {
  s_tempcolornames.insert(name, hex);
}

//-----------------------------------------------------------------------------

bool HexColorNames::parseText(QString text, TPixel &outPixel) {
  static QRegExp space("\\s");
  text.remove(space);
  if (text.size() == 0) return false;
  if (text[0] == "#") {
    text.remove(0, 1);
    return parseHexInternal(text, outPixel);
  }
  text = text.toLower();  // table names are lowercase

  // Find color from tables, user takes priority
  QMap<QString, QString>::const_iterator it;
  it = s_usercolornames.constFind(text);
  if (it == s_usercolornames.constEnd()) {
    it = s_maincolornames.constFind(text);
    if (it == s_maincolornames.constEnd()) return false;
  }

  QString hexText = it.value();
  hexText.remove(space);
  if (hexText[0] == "#") {
    hexText.remove(0, 1);
    return parseHexInternal(hexText, outPixel);
  }
  return false;
}

//-----------------------------------------------------------------------------

bool HexColorNames::parseHex(QString text, TPixel &outPixel) {
  static QRegExp space("\\s");
  text.remove(space);
  if (text.size() == 0) return false;
  if (text[0] == "#") {
    text.remove(0, 1);
  }
  return parseHexInternal(text, outPixel);
}

//-----------------------------------------------------------------------------

QString HexColorNames::generateHex(TPixel pixel) {
  if (pixel.m == 255) {
    // Opaque, omit alpha
    return QString("#%1%2%3")
        .arg(pixel.r, 2, 16, QLatin1Char('0'))
        .arg(pixel.g, 2, 16, QLatin1Char('0'))
        .arg(pixel.b, 2, 16, QLatin1Char('0'))
        .toUpper();
  } else {
    return QString("#%1%2%3%4")
        .arg(pixel.r, 2, 16, QLatin1Char('0'))
        .arg(pixel.g, 2, 16, QLatin1Char('0'))
        .arg(pixel.b, 2, 16, QLatin1Char('0'))
        .arg(pixel.m, 2, 16, QLatin1Char('0'))
        .toUpper();
  }
}

//-----------------------------------------------------------------------------

//*****************************************************************************
//  Hex line widget
//*****************************************************************************

TEnv::IntVar HexLineEditAutoComplete("HexLineEditAutoComplete", 1);

HexLineEdit::HexLineEdit(const QString &contents, QWidget *parent)
    : QLineEdit(contents, parent)
    , m_editing(false)
    , m_color(0, 0, 0)
    , m_completer(nullptr) {
  HexColorNames::loadMainFile(false);
  HexColorNames::loadUserFile(false);

  if (HexLineEditAutoComplete != 0) onAutoCompleteChanged(true);

  bool ret = true;

  ret = ret &&
        connect(HexColorNames::instance(), SIGNAL(autoCompleteChanged(bool)),
                this, SLOT(onAutoCompleteChanged(bool)));
  ret = ret && connect(HexColorNames::instance(), SIGNAL(colorsChanged()), this,
          SLOT(onColorsChanged()));
  assert(ret);
}

//-----------------------------------------------------------------------------

void HexLineEdit::updateColor() {
  setText(HexColorNames::generateHex(m_color));
}

//-----------------------------------------------------------------------------

void HexLineEdit::setStyle(TColorStyle &style, int index) {
  setColor(style.getColorParamValue(index));
}

//-----------------------------------------------------------------------------

void HexLineEdit::setColor(TPixel color) {
  if (m_color != color) {
    m_color = color;
    if (isVisible()) updateColor();
  }
}

//-----------------------------------------------------------------------------

bool HexLineEdit::fromText(QString text) {
  bool res = HexColorNames::parseText(text, m_color);
  if (res) updateColor();
  return res;
}

//-----------------------------------------------------------------------------

bool HexLineEdit::fromHex(QString text) {
  bool res = HexColorNames::parseHex(text, m_color);
  if (res) updateColor();
  return res;
}

//-----------------------------------------------------------------------------

void HexLineEdit::mousePressEvent(QMouseEvent *event) {
  QLineEdit::mousePressEvent(event);
  // Make Ctrl key disable select all so the user can click a specific character
  // after a focus-in, this likely will fall into a hidden feature thought.
  bool ctrlDown = event->modifiers() & Qt::ControlModifier;
  if (!m_editing && !ctrlDown) selectAll();
  m_editing = true;
}

//-----------------------------------------------------------------------------

void HexLineEdit::focusOutEvent(QFocusEvent *event) {
  QLineEdit::focusOutEvent(event);
  deselect();
  m_editing = false;
}

//-----------------------------------------------------------------------------

void HexLineEdit::showEvent(QShowEvent *event) {
  QLineEdit::showEvent(event);
  updateColor();
}

//-----------------------------------------------------------------------------

QCompleter *HexLineEdit::getCompleter() {
  QStringList autolist;

  // Build words list from all color names tables
  HexColorNames::iterator it;
  for (it = HexColorNames::beginMain(); it != HexColorNames::endMain(); ++it) {
    autolist.append(it.name());
  }
  for (it = HexColorNames::beginUser(); it != HexColorNames::endUser(); ++it) {
    autolist.append(it.name());
  }

  QCompleter *completer = new QCompleter(autolist);
  completer->setCaseSensitivity(Qt::CaseInsensitive);
  return completer;
}

//-----------------------------------------------------------------------------

void HexLineEdit::onAutoCompleteChanged(bool enable) {
  if (m_completer) {
    m_completer->deleteLater();
    setCompleter(nullptr);
    m_completer = nullptr;
  }
  if (enable) {
    m_completer = getCompleter();
    setCompleter(m_completer);
  }
}

void HexLineEdit::onColorsChanged() { onAutoCompleteChanged(true); }

//-----------------------------------------------------------------------------

//*****************************************************************************
//  Hex color names editor
//*****************************************************************************

HexColorNamesEditor::HexColorNamesEditor(QWidget *parent)
    : DVGui::Dialog(parent, true, false, "HexColorNamesEditor")
    , m_selectedItem(nullptr)
    , m_newEntry(false) {
  setWindowTitle(tr("Hex Color Names Editor"));
  setModal(false); // user may want to access main style editor and palettes

  QPushButton *okButton    = new QPushButton(tr("OK"), this);
  QPushButton *applyButton = new QPushButton(tr("Apply"), this);
  QPushButton *closeButton = new QPushButton(tr("Close"), this);

  m_unsColorButton = new QPushButton(tr("Unselect"));
  m_addColorButton = new QPushButton(tr("Add Color"));
  m_delColorButton = new QPushButton(tr("Delete Color"));
  m_hexLineEdit    = new HexLineEdit("", this);
  m_hexLineEdit->setObjectName("HexLineEdit");
  m_hexLineEdit->setFixedWidth(75);

  // Main default color names
  QGridLayout *mainLay = new QGridLayout();
  QWidget *mainTab = new QWidget();
  mainTab->setLayout(mainLay);

  m_mainTreeWidget = new QTreeWidget();
  m_mainTreeWidget->setColumnCount(2);
  m_mainTreeWidget->setColumnWidth(0, 175);
  m_mainTreeWidget->setColumnWidth(1, 50);
  m_mainTreeWidget->setHeaderLabels(QStringList() << "Name"
                                                  << "Hex value");
  mainLay->addWidget(m_mainTreeWidget, 0, 0);

  // User defined color names
  QGridLayout *userLay = new QGridLayout();
  QWidget *userTab     = new QWidget();
  userTab->setLayout(userLay);

  m_userTreeWidget = new QTreeWidget();
  m_userTreeWidget->setColumnCount(2);
  m_userTreeWidget->setColumnWidth(0, 175);
  m_userTreeWidget->setColumnWidth(1, 50);
  m_userTreeWidget->setHeaderLabels(QStringList() << "Name"
                                                  << "Hex value");
  m_colorField = new ColorField(this, true);
  userLay->addWidget(m_userTreeWidget, 0, 0, 1, 4);
  userLay->addWidget(m_unsColorButton, 1, 0);
  userLay->addWidget(m_addColorButton, 1, 1);
  userLay->addWidget(m_delColorButton, 1, 2);
  userLay->addWidget(m_hexLineEdit, 1, 3);
  userLay->addWidget(m_colorField, 2, 0, 1, 4);

  // Set delegate
  m_userEditingDelegate = new HexColorNamesEditingDelegate(m_userTreeWidget);
  m_mainTreeWidget->setItemDelegate(m_userEditingDelegate);
  m_userTreeWidget->setItemDelegate(m_userEditingDelegate);
  populateMainList(false);

  // Tabs
  QTabWidget *tab = new QTabWidget();
  tab->addTab(userTab, tr("User Defined Colors"));
  tab->addTab(mainTab, tr("Default Main Colors"));
  tab->setObjectName("hexTabWidget");

  // Bottom widgets
  QHBoxLayout *bottomLay = new QHBoxLayout();
  m_autoCompleteCb  = new QCheckBox(tr("Enable Auto-Complete"));
  m_autoCompleteCb->setChecked(HexLineEditAutoComplete != 0);
  m_autoCompleteCb->setSizePolicy(QSizePolicy::Expanding,
                                  QSizePolicy::Preferred);
  m_importButton = new QPushButton(tr("Import"));
  m_exportButton = new QPushButton(tr("Export"));
  bottomLay->setMargin(8);
  bottomLay->setSpacing(5);
  bottomLay->addWidget(m_autoCompleteCb);
  bottomLay->addWidget(m_importButton);
  bottomLay->addWidget(m_exportButton);

  m_topLayout->setContentsMargins(0, 0, 0, 0);
  m_topLayout->addWidget(tab);
  m_topLayout->addLayout(bottomLay);

  addButtonBarWidget(okButton, applyButton, closeButton);

  bool ret = true;

  ret = ret && connect(m_userEditingDelegate,
                       SIGNAL(editingStarted(const QModelIndex &)), this,
                       SLOT(onEditingStarted(const QModelIndex &)));
  ret = ret && connect(m_userEditingDelegate,
                       SIGNAL(editingFinished(const QModelIndex &)), this,
                       SLOT(onEditingFinished(const QModelIndex &)));
  ret = ret && connect(m_userEditingDelegate, SIGNAL(editingClosed()), this,
                       SLOT(onEditingClosed()));

  ret =
      ret &&
      connect(m_userTreeWidget,
              SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)),
              this,
              SLOT(onCurrentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)));
  ret =
      ret && connect(m_colorField, SIGNAL(colorChanged(const TPixel32 &, bool)),
                     this, SLOT(onColorFieldChanged(const TPixel32 &, bool)));
  ret = ret && connect(m_hexLineEdit, SIGNAL(editingFinished()), this,
                       SLOT(onHexChanged()));

  ret = ret &&
        connect(m_unsColorButton, SIGNAL(pressed()), this, SLOT(onDeselect()));
  ret = ret &&
        connect(m_addColorButton, SIGNAL(pressed()), this, SLOT(onAddColor()));
  ret = ret &&
        connect(m_delColorButton, SIGNAL(pressed()), this, SLOT(onDelColor()));
  ret =
      ret && connect(m_importButton, SIGNAL(pressed()), this, SLOT(onImport()));
  ret =
      ret && connect(m_exportButton, SIGNAL(pressed()), this, SLOT(onExport()));

  ret = ret && connect(okButton, SIGNAL(pressed()), this, SLOT(onOK()));
  ret = ret && connect(applyButton, SIGNAL(pressed()), this, SLOT(onApply()));
  ret = ret && connect(closeButton, SIGNAL(pressed()), this, SLOT(close()));
  assert(ret);
}

//-----------------------------------------------------------------------------

QTreeWidgetItem *HexColorNamesEditor::addEntry(QTreeWidget *tree,
                                               const QString &name,
                                               const QString &hex,
                                               bool editable) {
  TPixel pixel = TPixel(0, 0, 0);
  HexColorNames::parseHex(hex, pixel);

  QPixmap pixm(16, 16);
  pixm.fill(QColor(pixel.r, pixel.g, pixel.b, pixel.m));

  QTreeWidgetItem *treeItem = new QTreeWidgetItem(tree);
  treeItem->setText(0, name);
  treeItem->setIcon(1, QIcon(pixm));
  treeItem->setText(1, hex);
  if (!editable)
    treeItem->setFlags(treeItem->flags() & ~Qt::ItemIsSelectable);
  else
    treeItem->setFlags(treeItem->flags() | Qt::ItemIsEditable);

  return treeItem;
}

//-----------------------------------------------------------------------------

bool HexColorNamesEditor::updateUserHexEntry(QTreeWidgetItem *treeItem,
                                             const TPixel32 &color) {
  if (treeItem) {
    QPixmap pixm(16, 16);
    pixm.fill(QColor(color.r, color.g, color.b, color.m));

    treeItem->setIcon(1, QIcon(pixm));
    treeItem->setText(1, HexColorNames::generateHex(color));
    return true;
  }
  return false;
}

//-----------------------------------------------------------------------------

bool HexColorNamesEditor::nameValid(const QString& name) {
  if (name.isEmpty()) return false;
  return name.count(QRegExp("[\\\\#<>\"']")) == 0;
}

//-----------------------------------------------------------------------------

bool HexColorNamesEditor::nameExists(const QString &name,
                                     QTreeWidgetItem *self) {
  for (int i = 0; i < m_userTreeWidget->topLevelItemCount(); ++i) {
    QTreeWidgetItem *item = m_userTreeWidget->topLevelItem(i);
    if (item == self) continue;
    if (name.compare(item->text(0), Qt::CaseInsensitive) == 0) return true;
  }
  return false;
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::deselectItem(bool treeFocus) {
  if (m_newEntry) return;

  m_userTreeWidget->setCurrentItem(nullptr);
  if (treeFocus) m_userTreeWidget->setFocus();
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::deleteCurrentItem(bool deselect) {
  if (m_newEntry) return;

  QTreeWidgetItem *treeItem = m_userTreeWidget->currentItem();
  if (treeItem) delete treeItem;
  m_selectedItem = nullptr;
  if (deselect) m_userTreeWidget->setCurrentItem(nullptr);
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::populateMainList(bool reload) {
  HexColorNames::loadMainFile(reload);

  HexColorNames::iterator it;
  m_mainTreeWidget->clear();
  for (it = HexColorNames::beginMain(); it != HexColorNames::endMain(); ++it) {
    addEntry(m_mainTreeWidget, it.name(), it.value(), false);
  }
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::populateUserList(bool reload) {
  HexColorNames::loadUserFile(reload);

  HexColorNames::iterator it;
  m_userTreeWidget->clear();
  for (it = HexColorNames::beginUser(); it != HexColorNames::endUser(); ++it) {
    if (!nameExists(it.name(), nullptr))
      addEntry(m_userTreeWidget, it.name(), it.value(), true);
  }

  m_userTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder);
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::showEvent(QShowEvent *e) {
  populateUserList(false);

  deselectItem(false);
  m_delColorButton->setEnabled(false);
  m_unsColorButton->setEnabled(false);
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::keyPressEvent(QKeyEvent *event) {
  // Blocking dialog default actions is actually desirable
  // for example when user press Esc or Enter twice while
  // editing it might close the dialog by accident.

  if (!m_userTreeWidget->hasFocus()) return;

  switch (event->key()) {
  case Qt::Key_F5:
    populateMainList(true);
    populateUserList(true);
    m_mainTreeWidget->update();
    m_userTreeWidget->update();
    break;
  case Qt::Key_Escape:
    deselectItem(true);
    break;
  case Qt::Key_Delete:
    deleteCurrentItem(false);
    break;
  case Qt::Key_Insert:
    onAddColor();
    break;
  }
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::focusInEvent(QFocusEvent *e) {
  QWidget::focusInEvent(e);
  qApp->installEventFilter(this);
}

//--------------------------------------------------------

void HexColorNamesEditor::focusOutEvent(QFocusEvent *e) {
  QWidget::focusOutEvent(e);
  qApp->removeEventFilter(this);
}

//-----------------------------------------------------------------------------

bool HexColorNamesEditor::eventFilter(QObject *obj, QEvent *e) {
  if (e->type() == QEvent::Shortcut ||
      e->type() == QEvent::ShortcutOverride) {
      e->accept();
      return true;
  }
  return false;
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onCurrentItemChanged(QTreeWidgetItem *current,
                                               QTreeWidgetItem *previous) {
  m_selectedItem = current;
  m_delColorButton->setEnabled(current);
  m_unsColorButton->setEnabled(current);

  if (!current) return;
  m_selectedName = current->text(0);
  m_selectedHex  = current->text(1);
  m_selectedColn = 0;

  // Modify color field
  TPixel pixel(0, 0, 0);
  if (HexColorNames::parseHex(m_selectedHex, pixel)) {
    m_colorField->setColor(pixel);
    m_hexLineEdit->setColor(pixel);
  }
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onEditingStarted(const QModelIndex &modelIndex) {
  QTreeWidgetItem *item = (QTreeWidgetItem *)modelIndex.internalPointer();
  int column            = modelIndex.column();
  onItemStarted(item, column);
}

void HexColorNamesEditor::onEditingFinished(const QModelIndex &modelIndex) {
  QTreeWidgetItem *item = (QTreeWidgetItem *)modelIndex.internalPointer();
  int column            = modelIndex.column();
  onItemFinished(item, column);
}

void HexColorNamesEditor::onEditingClosed() {
  onItemFinished(m_selectedItem, m_selectedColn);
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onItemStarted(QTreeWidgetItem *item, int column) {
  m_selectedName = item->text(0);
  m_selectedHex  = item->text(1);
  m_selectedColn = 0;
  m_selectedItem = item;
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onItemFinished(QTreeWidgetItem *item, int column) {
  if (!m_selectedItem || !item) return;
  m_delColorButton->setEnabled(true);
  m_unsColorButton->setEnabled(true);

  try {
    if (m_selectedItem == item) {
      QString text = item->text(column);
      if (column == 0) {
        // Edit Name
        static QRegExp space("\\s");
        text.remove(space);
        text = text.toLower();
        if (text.isEmpty()) throw "";
        if (!nameValid(text))
          throw "Color name is not valid.\nFollowing characters can't be used: \\ # < > \" '";
        if (nameExists(text, item)) throw "Color name already exists.\nPlease use another name.";
        item->setText(0, text);
        m_userTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder);
      } else {
        // Edit Hex
        TPixel pixel;
        if (HexColorNames::parseHex(text, pixel)) {
          m_colorField->setColor(pixel);
          m_hexLineEdit->setColor(pixel);
          updateUserHexEntry(item, pixel);
        } else {
          item->setText(1, m_selectedHex);
        }
      }
    }
  } catch (const char *reason) {
    m_selectedItem = nullptr;
    if (m_selectedName.isEmpty()) {
      delete item;
    } else {
      item->setText(0, m_selectedName);
    }
    if (strlen(reason)) DVGui::warning(tr(reason));
  } catch (...) {
    m_selectedItem = nullptr;
    delete item;
  }
  m_newEntry = false;
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onColorFieldChanged(const TPixel32 &color,
                                              bool isDragging) {
  QTreeWidgetItem *treeItem = m_userTreeWidget->currentItem();
  if (updateUserHexEntry(treeItem, color)) {
    m_userTreeWidget->setCurrentItem(treeItem);
  }
  m_hexLineEdit->setColor(color);
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onHexChanged() {
  QTreeWidgetItem *treeItem = m_userTreeWidget->currentItem();
  if (m_hexLineEdit->fromText(m_hexLineEdit->text())) {
    TPixel pixel = m_hexLineEdit->getColor();
    updateUserHexEntry(treeItem, pixel);
    m_colorField->setColor(pixel);
  }
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onAddColor() {
  if (m_newEntry) return;

  TPixel pixel = m_colorField->getColor();
  QString hex = HexColorNames::generateHex(pixel);
  QTreeWidgetItem *treeItem = addEntry(m_userTreeWidget, "", hex, true);

  m_userTreeWidget->setCurrentItem(treeItem);
  onItemStarted(treeItem, 0);
  m_newEntry = true;
  m_userTreeWidget->editItem(treeItem, 0);

  m_delColorButton->setEnabled(false);
  m_unsColorButton->setEnabled(false);
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onDelColor() {
  if (m_newEntry) return;

  deleteCurrentItem(true);
  m_userTreeWidget->setFocus();
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onDeselect() { deselectItem(false); }

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onImport() {
  QString fileName = QFileDialog::getOpenFileName(
      this, tr("Open Color Names"), QString(),
      tr("Text or XML (*.txt *.xml);;Text files (*.txt);;XML files (*.xml)"));
  if (fileName.isEmpty()) return;

  QMessageBox::StandardButton ret = QMessageBox::question(
      this, tr("Hex Color Names Import"), tr("Do you want to merge with existing entries?"),
      QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No |
                                   QMessageBox::Cancel));
  if (ret == QMessageBox::Cancel) return;

  if (!HexColorNames::loadTempFile(TFilePath(fileName))) {
    DVGui::warning(tr("Error importing color names XML"));
  }
  HexColorNames::iterator it;
  if (ret == QMessageBox::No) m_userTreeWidget->clear();
  for (it = HexColorNames::beginTemp(); it != HexColorNames::endTemp(); ++it) {
    if (!nameExists(it.name(), nullptr))
      addEntry(m_userTreeWidget, it.name(), it.value(), true);
  }
  HexColorNames::clearTempEntries();
  m_userTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder);
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onExport() {
  QString fileName = QFileDialog::getSaveFileName(
      this, tr("Save Color Names"), QString(),
      tr("XML files (*.xml);;Text files (*.txt)"));
  if (fileName.isEmpty()) return;

  HexColorNames::clearTempEntries();
  for (int i = 0; i < m_userTreeWidget->topLevelItemCount(); ++i) {
    QTreeWidgetItem *item = m_userTreeWidget->topLevelItem(i);
    HexColorNames::setTempEntry(item->text(0), item->text(1));
  }
  if (!HexColorNames::saveTempFile(TFilePath(fileName))) {
    DVGui::warning(tr("Error exporting color names XML"));
  }
}

//-----------------------------------------------------------------------------

void HexColorNamesEditor::onOK() {
  onApply();
  close();
};

void HexColorNamesEditor::onApply() {
  HexColorNames::clearUserEntries();
  for (int i = 0; i < m_userTreeWidget->topLevelItemCount(); ++i) {
    QTreeWidgetItem *item = m_userTreeWidget->topLevelItem(i);
    HexColorNames::setUserEntry(item->text(0), item->text(1));
  }
  HexColorNames::saveUserFile();
  HexColorNames::instance()->emitChanged();

  bool oldAutoCompState = (HexLineEditAutoComplete != 0);
  bool newAutoCompState = m_autoCompleteCb->isChecked();
  if (oldAutoCompState != newAutoCompState) {
    HexLineEditAutoComplete = newAutoCompState ? 1 : 0;
    HexColorNames::instance()->emitAutoComplete(newAutoCompState);
  }
};

//-----------------------------------------------------------------------------