code:
#pragma once

/*
MIT License

Copyright (c) 2017 BalazsJako

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "imgui.h"
#include <array>
#include <map>
#include <memory>
#include <regex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

class TextEditor {
public:
  enum class PaletteIndex {
    Default,
    Keyword,
    Number,
    String,
    CharLiteral,
    Punctuation,
    Preprocessor,
    Identifier,
    KnownIdentifier,
    PreprocIdentifier,
    Comment,
    MultiLineComment,
    Background,
    Cursor,
    Selection,
    ErrorMarker,
    Breakpoint,
    LineNumber,
    CurrentLineFill,
    CurrentLineFillInactive,
    CurrentLineEdge,
    Max
  };

  enum class SelectionMode { Normal, Word, Line };

  struct Breakpoint {
    int mLine;
    bool mEnabled;
    std::string mCondition;

    Breakpoint()
    : mLine(-1)
    , mEnabled(false) {}
  };

  // Represents a character coordinate from the user's point of view,
  // i. e. consider an uniform grid (assuming fixed-width font) on the
  // screen as it is rendered, and each cell has its own coordinate, starting from 0.
  // Tabs are counted as [1..mTabSize] count empty spaces, depending on
  // how many space is necessary to reach the next tab stop.
  // For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4,
  // because it is rendered as "    ABC" on the screen.
  struct Coordinates {
    int mLine, mColumn;
    Coordinates()
    : mLine(0)
    , mColumn(0) {}
    Coordinates(int aLine, int aColumn)
    : mLine(aLine)
    , mColumn(aColumn) {
      assert(aLine >= 0);
      assert(aColumn >= 0);
    }
    static Coordinates Invalid() {
      static Coordinates invalid(-1, -1);
      return invalid;
    }

    bool operator==(const Coordinates& o) const { return mLine == o.mLine && mColumn == o.mColumn; }

    bool operator!=(const Coordinates& o) const { return mLine != o.mLine || mColumn != o.mColumn; }

    bool operator<(const Coordinates& o) const {
      if (mLine != o.mLine)
        return mLine < o.mLine;
      return mColumn < o.mColumn;
    }

    bool operator>(const Coordinates& o) const {
      if (mLine != o.mLine)
        return mLine > o.mLine;
      return mColumn > o.mColumn;
    }

    bool operator<=(const Coordinates& o) const {
      if (mLine != o.mLine)
        return mLine < o.mLine;
      return mColumn <= o.mColumn;
    }

    bool operator>=(const Coordinates& o) const {
      if (mLine != o.mLine)
        return mLine > o.mLine;
      return mColumn >= o.mColumn;
    }
  };

  struct Identifier {
    Coordinates mLocation;
    std::string mDeclaration;
  };

  typedef std::string String;
  typedef std::unordered_map<std::string, Identifier> Identifiers;
  typedef std::unordered_set<std::string> Keywords;
  typedef std::map<int, std::string> ErrorMarkers;
  typedef std::unordered_set<int> Breakpoints;
  typedef std::array<ImU32, (unsigned)PaletteIndex::Max> Palette;
  typedef uint8_t Char;

  struct Glyph {
    Char mChar;
    PaletteIndex mColorIndex = PaletteIndex::Default;
    bool mComment : 1;
    bool mMultiLineComment : 1;
    bool mPreprocessor : 1;

    Glyph(Char aChar, PaletteIndex aColorIndex)
    : mChar(aChar)
    , mColorIndex(aColorIndex)
    , mComment(false)
    , mMultiLineComment(false)
    , mPreprocessor(false) {}
  };

  typedef std::vector<Glyph> Line;
  typedef std::vector<Line> Lines;

  struct LanguageDefinition {
    typedef std::pair<std::string, PaletteIndex> TokenRegexString;
    typedef std::vector<TokenRegexString> TokenRegexStrings;
    typedef bool (*TokenizeCallback)(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex);

    std::string mName;
    Keywords mKeywords;
    Identifiers mIdentifiers;
    Identifiers mPreprocIdentifiers;
    std::string mCommentStart, mCommentEnd, mSingleLineComment;
    char mPreprocChar;
    bool mAutoIndentation;

    TokenizeCallback mTokenize;

    TokenRegexStrings mTokenRegexStrings;

    bool mCaseSensitive;

    LanguageDefinition()
    : mPreprocChar('#')
    , mAutoIndentation(true)
    , mTokenize(nullptr)
    , mCaseSensitive(true) {}

    static const LanguageDefinition& CPlusPlus();
    static const LanguageDefinition& Javascript();
    static const LanguageDefinition& HLSL();
    static const LanguageDefinition& GLSL();
    static const LanguageDefinition& C();
    static const LanguageDefinition& SQL();
    static const LanguageDefinition& AngelScript();
    static const LanguageDefinition& Lua();
  };

  TextEditor();
  ~TextEditor();

  void SetLanguageDefinition(const LanguageDefinition& aLanguageDef);
  const LanguageDefinition& GetLanguageDefinition() const { return mLanguageDefinition; }

  const Palette& GetPalette() const { return mPaletteBase; }
  void SetPalette(const Palette& aValue);

  void SetErrorMarkers(const ErrorMarkers& aMarkers) { mErrorMarkers = aMarkers; }
  void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; }

  void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false);
  void SetText(const std::string& aText);
  std::string GetText() const;

  void SetTextLines(const std::vector<std::string>& aLines);
  std::vector<std::string> GetTextLines() const;

  std::string GetSelectedText() const;
  std::string GetCurrentLineText() const;

  int GetTotalLines() const { return (int)mLines.size(); }
  bool IsOverwrite() const { return mOverwrite; }

  void SetReadOnly(bool aValue);
  bool IsReadOnly() const { return mReadOnly; }
  bool IsTextChanged() const { return mTextChanged; }
  bool IsCursorPositionChanged() const { return mCursorPositionChanged; }

  bool IsColorizerEnabled() const { return mColorizerEnabled; }
  void SetColorizerEnable(bool aValue);

  Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); }
  void SetCursorPosition(const Coordinates& aPosition);

  inline void SetHandleMouseInputs(bool aValue) { mHandleMouseInputs = aValue; }
  inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; }

  inline void SetHandleKeyboardInputs(bool aValue) { mHandleKeyboardInputs = aValue; }
  inline bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; }

  inline void SetImGuiChildIgnored(bool aValue) { mIgnoreImGuiChild = aValue; }
  inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; }

  inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; }
  inline bool IsShowingWhitespaces() const { return mShowWhitespaces; }

  void SetTabSize(int aValue);
  inline int GetTabSize() const { return mTabSize; }

  void InsertText(const std::string& aValue);
  void InsertText(const char* aValue);

  void MoveUp(int aAmount = 1, bool aSelect = false);
  void MoveDown(int aAmount = 1, bool aSelect = false);
  void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
  void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
  void MoveTop(bool aSelect = false);
  void MoveBottom(bool aSelect = false);
  void MoveHome(bool aSelect = false);
  void MoveEnd(bool aSelect = false);

  void SetSelectionStart(const Coordinates& aPosition);
  void SetSelectionEnd(const Coordinates& aPosition);
  void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal);
  void SelectWordUnderCursor();
  void SelectAll();
  bool HasSelection() const;

  void Copy();
  void Cut();
  void Paste();
  void Delete();

  bool CanUndo() const;
  bool CanRedo() const;
  void Undo(int aSteps = 1);
  void Redo(int aSteps = 1);

  static const Palette& GetDarkPalette();
  static const Palette& GetLightPalette();
  static const Palette& GetRetroBluePalette();

private:
  typedef std::vector<std::pair<std::regex, PaletteIndex>> RegexList;

  struct EditorState {
    Coordinates mSelectionStart;
    Coordinates mSelectionEnd;
    Coordinates mCursorPosition;
  };

  class UndoRecord {
  public:
    UndoRecord() {}
    ~UndoRecord() {}

    UndoRecord(const std::string& aAdded, const TextEditor::Coordinates aAddedStart, const TextEditor::Coordinates aAddedEnd,

               const std::string& aRemoved, const TextEditor::Coordinates aRemovedStart, const TextEditor::Coordinates aRemovedEnd,

               TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter);

    void Undo(TextEditor* aEditor);
    void Redo(TextEditor* aEditor);

    std::string mAdded;
    Coordinates mAddedStart;
    Coordinates mAddedEnd;

    std::string mRemoved;
    Coordinates mRemovedStart;
    Coordinates mRemovedEnd;

    EditorState mBefore;
    EditorState mAfter;
  };

  typedef std::vector<UndoRecord> UndoBuffer;

  void ProcessInputs();
  void Colorize(int aFromLine = 0, int aCount = -1);
  void ColorizeRange(int aFromLine = 0, int aToLine = 0);
  void ColorizeInternal();
  float TextDistanceToLineStart(const Coordinates& aFrom) const;
  void EnsureCursorVisible();
  int GetPageSize() const;
  std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const;
  Coordinates GetActualCursorCoordinates() const;
  Coordinates SanitizeCoordinates(const Coordinates& aValue) const;
  void Advance(Coordinates& aCoordinates) const;
  void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd);
  int InsertTextAt(Coordinates& aWhere, const char* aValue);
  void AddUndo(UndoRecord& aValue);
  Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const;
  Coordinates FindWordStart(const Coordinates& aFrom) const;
  Coordinates FindWordEnd(const Coordinates& aFrom) const;
  Coordinates FindNextWord(const Coordinates& aFrom) const;
  int GetCharacterIndex(const Coordinates& aCoordinates) const;
  int GetCharacterColumn(int aLine, int aIndex) const;
  int GetLineCharacterCount(int aLine) const;
  int GetLineMaxColumn(int aLine) const;
  bool IsOnWordBoundary(const Coordinates& aAt) const;
  void RemoveLine(int aStart, int aEnd);
  void RemoveLine(int aIndex);
  Line& InsertLine(int aIndex);
  void EnterCharacter(ImWchar aChar, bool aShift);
  void Backspace();
  void DeleteSelection();
  std::string GetWordUnderCursor() const;
  std::string GetWordAt(const Coordinates& aCoords) const;
  ImU32 GetGlyphColor(const Glyph& aGlyph) const;

  void HandleKeyboardInputs();
  void HandleMouseInputs();
  void Render();

  float mLineSpacing;
  Lines mLines;
  EditorState mState;
  UndoBuffer mUndoBuffer;
  int mUndoIndex;

  int mTabSize;
  bool mOverwrite;
  bool mReadOnly;
  bool mWithinRender;
  bool mScrollToCursor;
  bool mScrollToTop;
  bool mTextChanged;
  bool mColorizerEnabled;
  float mTextStart; // position (in pixels) where a code line starts relative to the left of the TextEditor.
  int mLeftMargin;
  bool mCursorPositionChanged;
  int mColorRangeMin, mColorRangeMax;
  SelectionMode mSelectionMode;
  bool mHandleKeyboardInputs;
  bool mHandleMouseInputs;
  bool mIgnoreImGuiChild;
  bool mShowWhitespaces;
  bool mTabsAsSpaces{true};
  int mTabSpaceCount = 2;

  Palette mPaletteBase;
  Palette mPalette;
  LanguageDefinition mLanguageDefinition;
  RegexList mRegexList;

  bool mCheckComments;
  Breakpoints mBreakpoints;
  ErrorMarkers mErrorMarkers;
  ImVec2 mCharAdvance;
  Coordinates mInteractiveStart, mInteractiveEnd;
  std::string mLineBuffer;
  uint64_t mStartTime;

  float mLastClick;
};

repo_name: Antidote/openrpgeditor

license: mit


code:
#include "Core/CommonUI/TilesetPicker.hpp"
#include "Core/MainWindow.hpp"

#include "Core/ImGuiExt/ImGuiUtils.hpp"
#include "imgui.h"
#include "imgui_internal.h"

#include <complex.h>

void TilesetPicker::setPage(int page) {
  m_page = page;
  m_palette.setPageIndex(m_page);
}
void TilesetPicker::drawPageButton(const std::string_view pageStr, const int page) {
  // For buttons, we always want to render autotiles and region tiles
  if (page != 0 && page != -1 && !m_palette.isPageValid(page)) {
    return;
  }

  ImGui::PushStyleColor(ImGuiCol_Button, m_page == page ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : ImGui::GetColorU32(ImGuiCol_Button));
  if (ImGui::Button(std::format("  {}  ", pageStr).c_str())) {
    setPage(page);
    m_palette.invalidateCursor();
  }
  ImGui::PopStyleColor();
}

void TilesetPicker::draw() {
  if (m_map != m_parent->currentMap()) {
    m_map = m_parent->currentMap();
    if (m_map != nullptr) {
      m_palette.setTilesetNames(Database::instance()->tilesets.tileset(m_map->tilesetId())->tilesetNames());
      while (!m_palette.isPageValid(m_page) && m_page > 0) {
        --m_page;
      }
      setPage(m_page);
    } else {
      m_palette.setTilesetNames({});
    }
  }

  m_palette.paintTiles();

  if (ImGui::Begin(std::format("{}###tilesets", trNOOP("Tilesets")).c_str())) {
    const auto calc = ImGui::CalcTextSize("A");
    ImGui::BeginChild("##tileset", {0, ImGui::GetContentRegionAvail().y - (calc.y + (ImGui::GetStyle().ItemSpacing.y * 3) + ImGui::GetStyle().FramePadding.y)}, 0,
                      ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_HorizontalScrollbar);
    {
      // We want to always render region tiles
      const ImVec2 cursorPos = (ImGui::GetMousePos() - ImGui::GetCurrentWindow()->ContentRegionRect.Min);
      if (m_palette.isPageValid(m_page) || m_page == -1) {
        ImGui::Image(static_cast<ImTextureID>(m_palette), static_cast<ImVec2>(m_palette.imageSize()));
        if ((ImGui::IsWindowFocused() || ImGui::IsWindowHovered()) && ImGui::IsItemHovered()) {
          if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
            // printf("Pick start!");
            m_palette.onCursorClicked({cursorPos.x, cursorPos.y});
            // printf(" %i %i\n", m_palette.cursorRect().x(), m_palette.cursorRect().y());
          }

          if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
            // printf("Cursor Drag!");
            m_palette.onCursorDrag({cursorPos.x, cursorPos.y});
            // printf(" %i %i %i %i\n", m_palette.cursorRect().x(), m_palette.cursorRect().y(), m_palette.cursorRect().width(), m_palette.cursorRect().height());
          }

          if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
            // printf("Cursor Release!\n");
            m_palette.onCursorReleased();
            auto penData = m_palette.penData();
            // auto size = m_palette.penSize();
            for (const auto& pen : penData) {
              // printf("%4i, %4i, %4i, %4i\n", pen[0], pen[1], pen[2], pen[3]);
            }
          }
        }

        if (m_palette.cursorRect().isValid()) {
          const auto* win = ImGui::GetCurrentWindow();
          const auto rect = m_palette.cursorPixelRect();
          const auto min = static_cast<ImVec2>(rect.topLeft());
          const auto max = static_cast<ImVec2>(rect.bottomRight());
          const auto contraction = ImGui::GetDPIScaledSize(1.5f, 1.5f);
          win->DrawList->AddRect(win->ContentRegionRect.Min + (min + contraction), win->ContentRegionRect.Min + (max - contraction), 0xFF000000, 0.f, 0, ImGui::GetDPIScaledValue(2.5f));
          win->DrawList->AddRect(win->ContentRegionRect.Min + (min + contraction), win->ContentRegionRect.Min + (max - contraction), 0xFFFFFFFF, 0.f, 0, ImGui::GetDPIScaledValue(1.5f));
        }
      }
    }
    ImGui::EndChild();
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {1.f, 1.f});
    ImGui::BeginHorizontal("##tileset_buttons", ImGui::GetContentRegionAvail());
    {
      ImGui::Spring(0.5f);
      drawPageButton("A"sv, 0);
      drawPageButton("B"sv, 1);
      drawPageButton("C"sv, 2);
      drawPageButton("D"sv, 3);
      drawPageButton("E"sv, 4);
      drawPageButton("R"sv, -1);
      ImGui::Spring(0.5f);
    }
    ImGui::EndHorizontal();
    ImGui::PopStyleVar();
  }

  ImGui::End();
}


code:
#pragma once
#include "Core/Tilemap/TilePalette.hpp"

class Map;
struct MainWindow;
struct TilesetPicker {
  explicit TilesetPicker(MainWindow* parent)
  : m_parent(parent) {}

  void setPage(int page);
  void drawPageButton(std::string_view pageStr, int page);
  void draw();

  const std::vector<std::array<int, 4>>& penData() const { return m_palette.penData(); }
  const Size& penSize() const { return m_palette.penSize(); }
  Rect cursorRect() const { return m_palette.cursorRect(); }
  bool isRegionMode() const { return m_palette.isRegionMode(); }
  bool isMapMode() const { return m_palette.isMapMode(); }

private:
  MainWindow* m_parent;
  TilePalette m_palette;
  int m_page = 0;
  Map* m_map = nullptr;
};


code:
#include "Core/CommonUI/TraitsEditor.hpp"
#include "Core/DatabaseEditor.hpp"

#include <format>

#include "imgui.h"

using namespace std::string_view_literals;

static constexpr auto TraitsEditorPopupId = "Traits Edit"sv;

void TraitsEditor::draw(DatabaseEditor* dbEditor) {
  if (m_traits == nullptr) {
    return;
  }

  ImGui::BeginGroup();
  {
    ImGui::SeparatorText("Traits");
    if (ImGui::BeginTable("##orpg_traits_editor", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollY)) {
      ImGui::TableSetupColumn("Type");
      ImGui::TableSetupColumn("Content");
      ImGui::TableHeadersRow();

      for (auto& trait : *m_traits) {
        ImGui::PushID(&trait);
        ImGui::TableNextRow();
        if (ImGui::TableNextColumn()) {
          if (ImGui::Selectable(std::format("{}##trait_{}", DecodeEnumName(trait.code()), reinterpret_cast<uintptr_t>(&trait)).c_str(), m_selectedTrait == &trait,
                                ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
            m_selectedTrait = &trait;
            if (ImGui::GetMouseClickedCount(ImGuiMouseButton_Left) >= 2) {
              m_isNewEntry = false;
              m_updateTab = true;
              m_tempTraitAssigned = false;
              ImGui::OpenPopup(TraitsEditorPopupId.data());
            }
          }
          if (ImGui::IsKeyPressed(ImGuiKey_Delete) && m_selectedTrait == &trait && m_deletingTrait == nullptr) {
            m_deletingTrait = &trait;
            m_selectedTrait = nullptr;
          }
          drawPopup(dbEditor);
        }

        if (ImGui::TableNextColumn()) {
          switch (trait.code()) {
          case TraitCode::Element_Rate: {
            ImGui::Text("%s * %i%%", Database::instance()->elementNameOrId(trait.dataId()).c_str(), static_cast<int>(trait.value() * 100));
            break;
          }
          case TraitCode::Ex_daa_Parameter: {
            ImGui::Text("%s + %i%%", DecodeEnumName(static_cast<EXParameterSource>(trait.dataId())).c_str(), static_cast<int>(trait.value() * 100));
            break;
          }
          case TraitCode::Sp_daa_Parameter:
            ImGui::Text("%s * %i%%", DecodeEnumName(static_cast<SPParameterSource>(trait.dataId())).c_str(), static_cast<int>(trait.value() * 100));
            break;
          case TraitCode::Parameter:
            ImGui::Text("%s * %i%%", DecodeEnumName(static_cast<ParameterSource>(trait.dataId())).c_str(), static_cast<int>(trait.value() * 100));
            break;
          case TraitCode::Debuff_Rate:
            ImGui::Text("%s * %i%%", DecodeEnumName(static_cast<DebuffSource>(trait.dataId())).c_str(), static_cast<int>(trait.value() * 100));
            break;
          case TraitCode::Attack_State:
          case TraitCode::State_Rate: {
            ImGui::Text("%s %c %i%%", Database::instance()->stateNameOrId(trait.dataId()).c_str(), trait.code() == TraitCode::Attack_State ? '+' : '*', static_cast<int>(trait.value() * 100));
            break;
          }
          case TraitCode::State_Resist: {
            ImGui::Text("%s", Database::instance()->stateNameOrId(trait.dataId()).c_str());
            break;
          }
          case TraitCode::Attack_Element: {
            ImGui::Text("%s", Database::instance()->elementNameOrId(trait.dataId()).c_str());
            break;
          }
          case TraitCode::Attack_Speed:
          case TraitCode::Attack_Times__plu_:
            ImGui::Text("%g", trait.value());
            break;
          case TraitCode::Add_Skill_Type:
          case TraitCode::Seal_Skill_Type: {
            ImGui::Text("%s", Database::instance()->skillTypeNameOrId(trait.dataId()).c_str());
            break;
          }
          case TraitCode::Add_Skill:
          case TraitCode::Seal_Skill: {
            ImGui::Text("%s", Database::instance()->skillNameOrId(trait.dataId()).c_str());
            break;
          }
          case TraitCode::Equip_Weapon: {
            ImGui::Text("%s", Database::instance()->weaponTypeNameOrId(trait.dataId()).c_str());
            break;
          }
          case TraitCode::Equip_Armor: {
            ImGui::Text("%s", Database::instance()->armorTypeOrId(trait.dataId()).c_str());
            break;
          }
          case TraitCode::Lock_Equip:
          case TraitCode::Seal_Equip: {
            ImGui::Text("%s", Database::instance()->equipTypeNameOrId(trait.dataId()).c_str());
            break;
          }
          case TraitCode::Slot_Type:
            ImGui::Text("%s", DecodeEnumName(static_cast<SlotType>(trait.dataId())).c_str());
            break;
          case TraitCode::Action_Times__plu_:
            ImGui::Text("%i%%", static_cast<int>(trait.value()) * 100);
            break;
          case TraitCode::Special_Flag:
            ImGui::Text("%s", DecodeEnumName(static_cast<SpecialFlag>(trait.dataId())).c_str());
            break;
          case TraitCode::Collapse_Effect:
            ImGui::Text("%s", DecodeEnumName(static_cast<CollapseEffect>(trait.dataId())).c_str());
            break;
          case TraitCode::Party_Ability:
            ImGui::Text("%s", DecodeEnumName(static_cast<PartyAbility>(trait.dataId())).c_str());
            break;
          default:
            ImGui::Text("%i %g", trait.dataId(), trait.value());
            break;
          }
        }

        ImGui::PopID();
      }

      /* Dummy entry for adding new traits */
      ImGui::TableNextRow();
      if (ImGui::TableNextColumn()) {
        if (ImGui::Selectable("##traits_editor_trait_dummy", false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
          if (ImGui::GetMouseClickedCount(ImGuiMouseButton_Left) >= 2) {
            m_tempTrait = Trait();
            m_isNewEntry = true;
            m_updateTab = true;
            m_tempTraitAssigned = false;
            m_selectedTrait = &m_tempTrait;
            ImGui::OpenPopup(TraitsEditorPopupId.data());
          }
        }
      }
      drawPopup(dbEditor);
      ImGui::EndTable();
    }
  }
  ImGui::EndGroup();

  if (m_deletingTrait) {
    std::erase_if(*m_traits, [&](const auto& trait) { return &trait == m_deletingTrait; });
    m_deletingTrait = nullptr;
  }
}

void TraitsEditor::drawRadioButton(const std::string_view name, const TraitCode code, const int dataId, const double initialValue) const {
  if (ImGui::RadioButton(name.data(), m_selectedTrait->code() == code)) {
    if (m_selectedTrait->code() != code) {
      m_selectedTrait->setDataId(dataId);
      m_selectedTrait->setValue(initialValue);
    }
    m_selectedTrait->setCode(code);
  }
}

void TraitsEditor::drawPopup(DatabaseEditor* dbEditor) {
  if (m_selectedTrait == nullptr || m_traits == nullptr) {
    return;
  }

  if (ImGui::BeginPopupModal(TraitsEditorPopupId.data(), nullptr, ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize)) {
    if (!m_isNewEntry && !m_tempTraitAssigned) {
      m_tempTraitAssigned = true;
    }
    ImGui::BeginGroup();
    {
      if (ImGui::BeginTabBar("##orpg_traits_edit_tab_bar")) {
        if (ImGui::BeginTabItem("Rate", nullptr,
                                m_selectedTrait->code() >= TraitCode::Element_Rate && m_selectedTrait->code() <= TraitCode::State_Resist && m_updateTab ? ImGuiTabItemFlags_SetSelected : 0)) {
          ImGui::BeginChild("##orpg_traits_rate_left_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, ImGuiWindowFlags_NoBackground);
          {
            if (ImGui::RadioButton("Element Rate", m_selectedTrait->code() == TraitCode::Element_Rate)) {
              if (m_selectedTrait->code() != TraitCode::Element_Rate) {
                m_selectedTrait->setDataId(1);
                m_selectedTrait->setValue(1);
              }
              m_selectedTrait->setCode(TraitCode::Element_Rate);
            }
            ImGui::NewLine();
            ImGui::Spacing();
            if (ImGui::RadioButton("Debuff Rate", m_selectedTrait->code() == TraitCode::Debuff_Rate)) {
              if (m_selectedTrait->code() != TraitCode::Debuff_Rate) {
                m_selectedTrait->setDataId(static_cast<int>(DebuffSource::Max_HP));
                m_selectedTrait->setValue(1);
              }
              m_selectedTrait->setCode(TraitCode::Debuff_Rate);
            }
            ImGui::NewLine();
            ImGui::Spacing();
            if (ImGui::RadioButton("State Rate", m_selectedTrait->code() == TraitCode::State_Rate)) {
              if (m_selectedTrait->code() != TraitCode::State_Rate) {
                m_selectedTrait->setDataId(1);
                m_selectedTrait->setValue(1);
              }
              m_selectedTrait->setCode(TraitCode::State_Rate);
            }
            ImGui::NewLine();
            ImGui::Spacing();
            if (ImGui::RadioButton("State Resist", m_selectedTrait->code() == TraitCode::State_Resist)) {
              if (m_selectedTrait->code() != TraitCode::State_Resist) {
                m_selectedTrait->setDataId(1);
                m_selectedTrait->setValue(0);
              }
              m_selectedTrait->setCode(TraitCode::State_Resist);
            }
          }
          ImGui::EndChild();
          ImGui::SameLine();
          ImGui::BeginChild("##orpg_traits_rate_right_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Element_Rate);
            {
              const auto element = Database::instance()->elementNameOrId(m_selectedTrait->dataId());
              if (ImGui::BeginCombo("##trait_element_rate_combo", m_selectedTrait->code() == TraitCode::Element_Rate ? element.c_str() : "")) {
                for (int i = 1; i < Database::instance()->system.elements().size(); ++i) {
                  if (ImGui::Selectable(Database::instance()->elementNameOrId(i).c_str(), i == m_selectedTrait->dataId())) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
              int tmpInt = m_selectedTrait->code() == TraitCode::Element_Rate ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_element_rate_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::Element_Rate ? "* %d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Debuff_Rate);
            {
              if (ImGui::BeginCombo("##trait_debuff_rate_combo",
                                    m_selectedTrait->code() == TraitCode::Debuff_Rate ? DecodeEnumName(static_cast<DebuffSource>(m_selectedTrait->dataId())).c_str() : "")) {
                for (auto v : magic_enum::enum_values<DebuffSource>()) {
                  if (ImGui::Selectable(DecodeEnumName(v).c_str(), static_cast<DebuffSource>(m_selectedTrait->dataId()) == v)) {
                    m_selectedTrait->setDataId(static_cast<int>(v));
                  }
                }
                ImGui::EndCombo();
              }
              int tmpInt = m_selectedTrait->code() == TraitCode::Debuff_Rate ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_debuff_rate_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::Debuff_Rate ? "* %d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::State_Rate);
            {
              if (ImGui::Button(m_selectedTrait->code() == TraitCode::State_Rate ? Database::instance()->stateNameOrId(m_selectedTrait->dataId()).c_str() : "##trait_state_rate_selection",
                                ImVec2{ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x, 0})) {
                m_statePicker.emplace("States"sv, Database::instance()->states.states(), m_selectedTrait->dataId());
                m_statePicker->setOpen(true);
              }
              int tmpInt = m_selectedTrait->code() == TraitCode::State_Rate ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_state_rate_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::State_Rate ? "* %d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::State_Resist);
            {
              if (ImGui::Button(m_selectedTrait->code() == TraitCode::State_Resist ? Database::instance()->stateNameOrId(m_selectedTrait->dataId()).c_str() : "##trait_state_resist_selection",
                                ImVec2{ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x, 0})) {
                m_statePicker.emplace("States"sv, Database::instance()->states.states(), m_selectedTrait->dataId());
                m_statePicker->setOpen(true);
              }
            }
            ImGui::EndDisabled();
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Param", nullptr,
                                m_selectedTrait->code() >= TraitCode::Parameter && m_selectedTrait->code() <= TraitCode::Sp_daa_Parameter && m_updateTab ? ImGuiTabItemFlags_SetSelected : 0)) {
          ImGui::BeginChild("##orpg_traits_param_left_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            drawRadioButton("Parameter"sv, TraitCode::Parameter, static_cast<int>(ParameterSource::Max_HP), 1);
            ImGui::NewLine();
            ImGui::Spacing();
            drawRadioButton("Ex-Parameter"sv, TraitCode::Ex_daa_Parameter, static_cast<int>(EXParameterSource::Hit_Rate), 0);
            ImGui::NewLine();
            ImGui::Spacing();
            drawRadioButton("Sp-Parameter"sv, TraitCode::Sp_daa_Parameter, static_cast<int>(SPParameterSource::Target_Rate), 1);
          }
          ImGui::EndChild();
          ImGui::SameLine();
          ImGui::BeginChild("##orpg_traits_param_right_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Parameter);
            {
              if (ImGui::BeginCombo("##trait_parameter_combo",
                                    m_selectedTrait->code() == TraitCode::Parameter ? DecodeEnumName(static_cast<ParameterSource>(m_selectedTrait->dataId())).c_str() : "")) {
                for (auto v : magic_enum::enum_values<ParameterSource>()) {
                  if (ImGui::Selectable(DecodeEnumName(v).c_str(), static_cast<ParameterSource>(m_selectedTrait->dataId()) == v)) {
                    m_selectedTrait->setDataId(static_cast<int>(v));
                  }
                }
                ImGui::EndCombo();
              }
              int tmpInt = m_selectedTrait->code() == TraitCode::Parameter ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_parameter_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::Parameter ? "* %d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Ex_daa_Parameter);
            {
              if (ImGui::BeginCombo("##trait_exparameter_combo",
                                    m_selectedTrait->code() == TraitCode::Ex_daa_Parameter ? DecodeEnumName(static_cast<EXParameterSource>(m_selectedTrait->dataId())).c_str() : "")) {
                for (auto v : magic_enum::enum_values<EXParameterSource>()) {
                  if (ImGui::Selectable(DecodeEnumName(v).c_str(), static_cast<EXParameterSource>(m_selectedTrait->dataId()) == v)) {
                    m_selectedTrait->setDataId(static_cast<int>(v));
                  }
                }
                ImGui::EndCombo();
              }
              int tmpInt = m_selectedTrait->code() == TraitCode::Ex_daa_Parameter ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_exparameter_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::Ex_daa_Parameter ? "+ %d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Sp_daa_Parameter);
            {
              if (ImGui::BeginCombo("##trait_spparameter_combo",
                                    m_selectedTrait->code() == TraitCode::Sp_daa_Parameter ? DecodeEnumName(static_cast<SPParameterSource>(m_selectedTrait->dataId())).c_str() : "")) {
                for (auto v : magic_enum::enum_values<SPParameterSource>()) {
                  if (ImGui::Selectable(DecodeEnumName(v).c_str(), static_cast<SPParameterSource>(m_selectedTrait->dataId()) == v)) {
                    m_selectedTrait->setDataId(static_cast<int>(v));
                  }
                }
                ImGui::EndCombo();
              }
              int tmpInt = m_selectedTrait->code() == TraitCode::Sp_daa_Parameter ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_spparameter_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::Sp_daa_Parameter ? "* %d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Attack", nullptr,
                                m_selectedTrait->code() >= TraitCode::Attack_Element && m_selectedTrait->code() <= TraitCode::Attack_Times__plu_ && m_updateTab ? ImGuiTabItemFlags_SetSelected : 0)) {
          ImGui::BeginChild("##orpg_traits_attack_left_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            drawRadioButton("Attack Element"sv, TraitCode::Attack_Element, 1, 0);
            drawRadioButton("Attack State"sv, TraitCode::Attack_State, 1, 1);
            ImGui::NewLine();
            ImGui::Spacing();
            drawRadioButton("Attack Speed"sv, TraitCode::Attack_Speed, 1, 0);
            drawRadioButton("Attack Times +"sv, TraitCode::Attack_Times__plu_, 1, 0);
          }
          ImGui::EndChild();
          ImGui::SameLine();
          ImGui::BeginChild("##orpg_traits_attack_right_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Attack_Element);
            {
              auto element = Database::instance()->system.element(m_selectedTrait->dataId());
              if (ImGui::BeginCombo("##trait_attack_elem_combo", m_selectedTrait->code() == TraitCode::Attack_Element && !element.empty() ? element.c_str() : "")) {
                for (int i = 1; i < Database::instance()->system.elements().size(); ++i) {
                  auto elem = Database::instance()->system.element(i);
                  if (ImGui::Selectable(!elem.empty() ? elem.c_str() : "##traits_editor_empty_attack_elem", m_selectedTrait->dataId() == i)) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Attack_State);
            {
              if (ImGui::Button(m_selectedTrait->code() == TraitCode::Attack_State ? Database::instance()->stateNameOrId(m_selectedTrait->dataId()).c_str() : "##trait_attack_state_selection",
                                ImVec2{ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x, 0})) {
                m_statePicker.emplace("States"sv, Database::instance()->states.states(), m_selectedTrait->dataId());
                m_statePicker->setOpen(true);
              }

              int tmpInt = m_selectedTrait->code() == TraitCode::Attack_State ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_attack_state_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::Attack_State ? "* %d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Attack_Speed);
            {
              float tmp = m_selectedTrait->code() == TraitCode::Attack_Speed ? static_cast<float>(m_selectedTrait->value()) : 0;
              if (ImGui::SliderFloat("##trait_attack_speed_value", &tmp, -1000, 1000, m_selectedTrait->code() == TraitCode::Attack_Speed ? "%g" : "")) {
                m_selectedTrait->setValue(tmp);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Attack_Times__plu_);
            {
              float tmp = m_selectedTrait->code() == TraitCode::Attack_Times__plu_ ? static_cast<float>(m_selectedTrait->value()) : 0;
              if (ImGui::SliderFloat("##trait_attack_times+_value", &tmp, -9, 9, m_selectedTrait->code() == TraitCode::Attack_Times__plu_ ? "%g" : "")) {
                m_selectedTrait->setValue(tmp);
              }
            }
            ImGui::EndDisabled();
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Skill", nullptr,
                                m_selectedTrait->code() >= TraitCode::Add_Skill_Type && m_selectedTrait->code() <= TraitCode::Seal_Skill && m_updateTab ? ImGuiTabItemFlags_SetSelected : 0)) {
          ImGui::BeginChild("##orpg_traits_skill_left_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            drawRadioButton("Add Skill Type"sv, TraitCode::Add_Skill_Type, 1, 0);
            drawRadioButton("Seal Skill Type"sv, TraitCode::Seal_Skill_Type, 1, 0);
            drawRadioButton("Add Skill"sv, TraitCode::Add_Skill, 1, 0);
            drawRadioButton("Seal Skill"sv, TraitCode::Seal_Skill, 1, 0);
          }
          ImGui::EndChild();
          ImGui::SameLine();
          ImGui::BeginChild("##orpg_traits_skill_right_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Add_Skill_Type);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Add_Skill_Type ? Database::instance()->skillTypeNameOrId(m_selectedTrait->dataId()) : "";
              if (ImGui::BeginCombo("##trait_editor_add_skill_type_combo", preview.c_str())) {
                for (int i = 1; i < Database::instance()->system.skillTypes().size(); i++) {
                  if (ImGui::Selectable(Database::instance()->skillTypeNameOrId(i).c_str(), i == m_selectedTrait->dataId())) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Seal_Skill_Type);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Seal_Skill_Type ? Database::instance()->skillTypeNameOrId(m_selectedTrait->dataId()) : "";
              if (ImGui::BeginCombo("##trait_editor_seal_skill_type_combo", preview.c_str())) {
                for (int i = 1; i < Database::instance()->system.skillTypes().size(); i++) {
                  if (ImGui::Selectable(Database::instance()->skillTypeNameOrId(i).c_str(), i == m_selectedTrait->dataId())) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Add_Skill);
            {
              if (ImGui::Button(m_selectedTrait->code() == TraitCode::Add_Skill ? Database::instance()->skillNameOrId(m_selectedTrait->dataId()).c_str() : "##trait_add_skill_selection",
                                ImVec2{ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x, 0})) {
                m_skillPicker.emplace("Skills"sv, Database::instance()->skills.skills(), m_selectedTrait->dataId());
                m_skillPicker->setOpen(true);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Seal_Skill);
            {
              if (ImGui::Button(m_selectedTrait->code() == TraitCode::Seal_Skill ? Database::instance()->skillNameOrId(m_selectedTrait->dataId()).c_str() : "##trait_seal_skill_selection",
                                ImVec2{ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x, 0})) {
                m_skillPicker.emplace("Skills"sv, Database::instance()->skills.skills(), m_selectedTrait->dataId());
                m_skillPicker->setOpen(true);
              }
            }
            ImGui::EndDisabled();
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Equip", nullptr,
                                m_selectedTrait->code() >= TraitCode::Equip_Weapon && m_selectedTrait->code() <= TraitCode::Slot_Type && m_updateTab ? ImGuiTabItemFlags_SetSelected : 0)) {
          ImGui::BeginChild("##orpg_traits_equip_left_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            drawRadioButton("Equip Weapon"sv, TraitCode::Equip_Weapon, 1, 0);
            drawRadioButton("Equip Armor"sv, TraitCode::Equip_Armor, 1, 0);
            drawRadioButton("Lock Equip"sv, TraitCode::Lock_Equip, 1, 0);
            drawRadioButton("Seal Equip"sv, TraitCode::Seal_Equip, 1, 0);
            drawRadioButton("Slot Type"sv, TraitCode::Slot_Type, 1, 0);
          }
          ImGui::EndChild();
          ImGui::SameLine();
          ImGui::BeginChild("##orpg_traits_equip_right_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Equip_Weapon);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Equip_Weapon ? Database::instance()->weaponTypeNameOrId(m_selectedTrait->dataId()) : "";
              if (ImGui::BeginCombo("##trait_editor_equip_weapon_type_combo", preview.c_str())) {
                for (int i = 1; i < Database::instance()->system.weaponTypes().size(); i++) {
                  if (ImGui::Selectable(Database::instance()->weaponTypeNameOrId(i).c_str(), i == m_selectedTrait->dataId())) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Equip_Armor);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Equip_Armor ? Database::instance()->armorTypeOrId(m_selectedTrait->dataId()) : "";
              if (ImGui::BeginCombo("##trait_editor_equip_armor_type_combo", preview.c_str())) {
                for (int i = 1; i < Database::instance()->system.armorTypes().size(); i++) {
                  if (ImGui::Selectable(Database::instance()->armorTypeOrId(i).c_str(), i == m_selectedTrait->dataId())) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Lock_Equip);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Lock_Equip ? Database::instance()->equipTypeNameOrId(m_selectedTrait->dataId()) : "";
              if (ImGui::BeginCombo("##trait_editor_equip_type_combo", preview.c_str())) {
                for (int i = 1; i < Database::instance()->system.equipTypes().size(); i++) {
                  if (ImGui::Selectable(Database::instance()->equipTypeNameOrId(i).c_str(), i == m_selectedTrait->dataId())) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Seal_Equip);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Seal_Equip ? Database::instance()->equipTypeNameOrId(m_selectedTrait->dataId()) : "";
              if (ImGui::BeginCombo("##trait_editor_seal_equip_type_combo", preview.c_str())) {
                for (int i = 1; i < Database::instance()->system.equipTypes().size(); i++) {
                  if (ImGui::Selectable(Database::instance()->equipTypeNameOrId(i).c_str(), i == m_selectedTrait->dataId())) {
                    m_selectedTrait->setDataId(i);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Slot_Type);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Slot_Type ? DecodeEnumName(static_cast<SlotType>(m_selectedTrait->dataId())) : "";
              if (ImGui::BeginCombo("##trait_editor_slot_type_combo", preview.c_str())) {
                for (const auto& type : magic_enum::enum_values<SlotType>()) {
                  if (ImGui::Selectable(DecodeEnumName(type).c_str(), m_selectedTrait->dataId() == static_cast<int>(type))) {
                    m_selectedTrait->setDataId(static_cast<int>(type));
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Other", nullptr,
                                m_selectedTrait->code() >= TraitCode::Action_Times__plu_ && m_selectedTrait->code() <= TraitCode::Party_Ability && m_updateTab ? ImGuiTabItemFlags_SetSelected : 0)) {
          ImGui::BeginChild("##orpg_traits_other_left_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            drawRadioButton("Action Times +", TraitCode::Action_Times__plu_, 0, 1);
            drawRadioButton("Special Flags"sv, TraitCode::Action_Times__plu_, 0, 0);
            drawRadioButton("Collapse Effect"sv, TraitCode::Collapse_Effect, 0, 0);
            drawRadioButton("Party Ability"sv, TraitCode::Party_Ability, 0, 0);
          }
          ImGui::EndChild();
          ImGui::SameLine();
          ImGui::BeginChild("##orpg_traits_other_right_child", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize,
                            ImGuiWindowFlags_NoBackground);
          {
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Action_Times__plu_);
            {
              int tmpInt = m_selectedTrait->code() == TraitCode::Action_Times__plu_ ? static_cast<int>(m_selectedTrait->value() * 100) : 0;
              if (ImGui::SliderInt("##trait_action_times_plus_value", &tmpInt, 0, 100, m_selectedTrait->code() == TraitCode::Action_Times__plu_ ? "%d%%" : "")) {
                m_selectedTrait->setValue(tmpInt / 100.0);
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Special_Flag);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Special_Flag ? DecodeEnumName(static_cast<SpecialFlag>(m_selectedTrait->dataId())) : "";
              if (ImGui::BeginCombo("##trait_editor_special_flag_combo", preview.c_str())) {
                for (const auto& type : magic_enum::enum_values<SpecialFlag>()) {
                  if (ImGui::Selectable(DecodeEnumName(type).c_str(), m_selectedTrait->dataId() == static_cast<int>(type))) {
                    m_selectedTrait->setDataId(static_cast<int>(type));
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Collapse_Effect);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Collapse_Effect ? DecodeEnumName(static_cast<CollapseEffect>(m_selectedTrait->dataId())) : "";
              if (ImGui::BeginCombo("##trait_editor_collapse_effect_combo", preview.c_str())) {
                for (const auto& type : magic_enum::enum_values<CollapseEffect>()) {
                  if (ImGui::Selectable(DecodeEnumName(type).c_str(), m_selectedTrait->dataId() == static_cast<int>(type))) {
                    m_selectedTrait->setDataId(static_cast<int>(type));
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_selectedTrait->code() != TraitCode::Party_Ability);
            {
              const auto preview = m_selectedTrait->code() == TraitCode::Party_Ability ? DecodeEnumName(static_cast<PartyAbility>(m_selectedTrait->dataId())) : "";
              if (ImGui::BeginCombo("##trait_editor_party_ability_combo", preview.c_str())) {
                for (const auto& type : magic_enum::enum_values<PartyAbility>()) {
                  if (ImGui::Selectable(DecodeEnumName(type).c_str(), m_selectedTrait->dataId() == static_cast<int>(type))) {
                    m_selectedTrait->setDataId(static_cast<int>(type));
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndDisabled();
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }
        ImGui::EndTabBar();
      }
    }
    ImGui::EndGroup();
    ImGui::Separator();
    ImGui::BeginGroup();
    {
      if (ImGui::Button("OK")) {
        if (m_isNewEntry) {
          m_traits->push_back(m_tempTrait);
        }

        m_selectedTrait->acceptChanges();
        // TODO: Undo stack with m_selectedTrait->serializeOldValues
        m_isNewEntry = false;
        ImGui::CloseCurrentPopup();
      }
      ImGui::SameLine();
      if (ImGui::Button("Cancel")) {
        if (!m_isNewEntry) {
          // Restore values to their unmodified state;
          m_selectedTrait->restoreOriginal();
        }
        m_isNewEntry = false;
        ImGui::CloseCurrentPopup();
      }
    }
    ImGui::EndGroup();

    if (m_statePicker) {
      if (auto [closed, confirmed] = m_statePicker->draw(); closed) {
        if (confirmed) {
          m_selectedTrait->setDataId(m_statePicker->selection());
        }
        m_statePicker.reset();
      }
    }

    if (m_skillPicker) {
      if (auto [closed, confirmed] = m_skillPicker->draw(); closed) {
        if (confirmed) {
          m_selectedTrait->setDataId(m_skillPicker->selection());
        }
        m_skillPicker.reset();
      }
    }
    ImGui::EndPopup();
  }
  m_updateTab = false;
}


code:
#pragma once

#include "Core/CommonUI/ObjectPicker.hpp"
#include "Database/Trait.hpp"

#include <vector>

class State;
class Skill;
class DatabaseEditor;

class TraitsEditor final {
public:
  void setTraits(std::vector<Trait>* traits) { m_traits = traits; }
  void draw(DatabaseEditor* dbEditor);
  void drawRadioButton(std::string_view name, TraitCode code, int dataId, double initialValue) const;

private:
  void drawPopup(DatabaseEditor* dbEditor);
  std::vector<Trait>* m_traits = nullptr;
  Trait* m_selectedTrait = nullptr;
  bool m_updateTab{false};
  bool m_isNewEntry{false};
  bool m_tempTraitAssigned{false};
  Trait m_tempTrait;
  Trait* m_deletingTrait = nullptr;
  std::optional<ObjectPicker<State>> m_statePicker;
  std::optional<ObjectPicker<Skill>> m_skillPicker;
};


code:
#include "Core/CommonUI/VariableSwitchPicker.hpp"

#include "imgui.h"
#include "misc/cpp/imgui_stdlib.h"
#include <format>

#include <algorithm>

static bool ContainsCaseInsensitive(std::string_view str, std::string_view val) {
  return std::search(str.begin(), str.end(), val.begin(), val.end(), [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); }) != str.end();
}

VariableSwitchPicker::VariableSwitchPicker(const std::string_view name, const std::vector<std::string>& values, const int initialSelection, int rangeStart)
: IDialogController(name)
, m_list(&values)
, m_selection(initialSelection) {
  for (int i = rangeStart; i < m_list->size(); ++i) {
    m_trackedValues.emplace_back(i, &(*m_list)[i]);
  }
  if (m_selection > 0) {
    m_navigateOnOpen = true;
  }
}

std::tuple<bool, bool> VariableSwitchPicker::draw() {
  const std::string title = std::format("{} Selection###variable_switch_picker", m_name);
  if (isOpen()) {
    ImGui::OpenPopup("###variable_switch_picker");
  }

  const ImVec2 center = ImGui::GetMainViewport()->GetCenter();
  ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
  ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size / 2, ImGuiCond_Once);
  if (ImGui::BeginPopupModal(title.c_str(), &m_open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings)) {
    ImGui::Text("Filter");
    ImGui::SameLine();
    ImGui::InputText("##variable_switch_selection_filter_input", &m_filter);
    ImGui::SameLine();
    if (ImGui::Button("Clear")) {
      m_filter.clear();
    }
    const auto calc = ImGui::CalcTextSize("OKCANCEL");
    if (ImGui::BeginTable("Objects", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_ScrollY,
                          ImVec2{0, ImGui::GetContentRegionAvail().y - (calc.y + (ImGui::GetStyle().ItemSpacing.y * 3) + ImGui::GetStyle().FramePadding.y)})) {

      ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_PreferSortAscending | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed, 0, 1);
      ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0, 2);
      ImGui::TableSetupScrollFreeze(0, 1);
      ImGui::TableHeadersRow();

      ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs();

      bool hasSortSpec = sortSpecs != nullptr && sortSpecs->SpecsCount == 1;

      std::vector<std::pair<int, const std::string*>> sortedList;
      for (auto& [id, name] : m_trackedValues) {
        if (!m_filter.empty()) {
          if (ContainsCaseInsensitive(*name, m_filter) || ContainsCaseInsensitive(std::to_string(id), m_filter)) {
            sortedList.emplace_back(id, name);
          }
        } else {
          sortedList.emplace_back(id, name);
        }
      }

      if (hasSortSpec) {
        const auto& spec = sortSpecs->Specs[0];
        if (spec.ColumnUserID == 1) {
          std::sort(sortedList.begin(), sortedList.end(), [&](auto& a, auto& b) { return spec.SortDirection == ImGuiSortDirection_Ascending ? a.first < b.first : a.first > b.first; });
        } else if (spec.ColumnUserID == 2) {
          std::sort(sortedList.begin(), sortedList.end(), [&](auto& a, auto& b) {
            int compare = a.second->compare(*b.second);
            return spec.SortDirection == ImGuiSortDirection_Ascending ? compare < 0 : compare > 0;
          });
        }
      }

      for (auto& [id, value] : sortedList) {
        ImGui::PushID(value);
        ImGui::TableNextRow();

        /* ID */
        if (ImGui::TableNextColumn()) {
          std::string idStr = std::format("{:04}", id);
          if (ImGui::Selectable(idStr.c_str(), m_selection == id, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
            m_selection = id;
            if (ImGui::GetMouseClickedCount(ImGuiMouseButton_Left) >= 2) {
              m_open = false;
              m_confirmed = true;
            }
          }
          if (m_selection == id) {
            ImGui::SetItemDefaultFocus();
            if (m_navigateOnOpen) {
              ImGui::SetScrollHereY();
              m_navigateOnOpen = false;
            }
          }
        }
        /* Name */
        if (ImGui::TableNextColumn()) {
          ImGui::Text("%s", value->c_str());
        }
        ImGui::PopID();
      }

      ImGui::EndTable();
    }
    ImGui::BeginGroup();
    {
      if (ImGui::Button("OK")) {
        m_confirmed = true;
        m_open = false;
        ImGui::CloseCurrentPopup();
      }
      ImGui::SameLine();
      if (ImGui::Button("Cancel")) {
        m_confirmed = false;
        m_open = false;
        ImGui::CloseCurrentPopup();
      }
    }
    ImGui::EndGroup();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once

#include "IDialogController.hpp"

#include <string>
#include <vector>

class VariableSwitchPicker : public IDialogController {
public:
  VariableSwitchPicker(const std::string_view name, const std::vector<std::string>& values, int selection, int rangeStart = 1);

  int selection() const { return m_selection; }
  void setSelection(const int selection) { m_selection = selection; }

  std::tuple<bool, bool> draw() override;

private:
  const std::vector<std::string>* m_list;
  std::vector<std::pair<int, const std::string*>> m_trackedValues;
  int m_selection{1};
  bool m_navigateOnOpen{false};
  std::string m_filter;
};


code:
#include "Core/DatabaseEditor.hpp"

#include "imgui.h"
#include "imgui_internal.h"
#include "ImGuiExt/ImGuiUtils.hpp"

void DatabaseEditor::draw() {
  if (isReady() && !onReady.is_empty()) {
    emit_signal(onReady);
  } else if (!isReady()) {
    return;
  }

  if (!m_isOpen) {
    return;
  }

  if (m_currentTab == nullptr && m_actors) {
    m_currentTab = &m_actors.value();
  }

  ImGui::SetNextWindowSizeConstraints(ImGui::GetDPIScaledSize(800, 720), ImVec2{FLT_MAX, FLT_MAX});
  ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size / 2, ImGuiCond_Appearing);
  ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, {0.5, 0.5});
  if (ImGui::Begin(std::format("{}###databaseeditor", trNOOP("Database")).c_str(), &m_isOpen)) {
    ImGui::SetNextItemWidth(ImGui::GetDPIScaledValue(100.f));
    const auto calc = ImGui::CalcTextSize("ABCDEFGHIJKLMNOPQR").x;
    ImGui::BeginChild("##orpg_database_editor_tab_buttons", ImVec2{calc + ImGui::GetStyle().ItemSpacing.x, 0}, 0, ImGuiWindowFlags_NoBackground);

    ImGui::SetNextItemWidth(ImGui::GetDPIScaledValue(160.f));
    int index{0};
    if (m_filterByHeader) {
      if (ImGui::BeginCombo("##orpg_database_editor_header_list", m_selectedHeaderIndex == -1 ? "" : m_currentTab->getName(m_currentTab->getHeader(m_selectedHeaderIndex)).c_str())) {
        char buf[1024];
        for (int v : m_currentTab->getHeaders()) {
          strncpy(buf, m_commonEvents.value().event(v)->name().c_str(), 1024);
          if (ImGui::Selectable(buf, m_selectedHeaderIndex == index)) {
            m_selectedHeaderIndex = index;
            m_currentTab->setHeaderRange(m_currentTab->getHeader(m_selectedHeaderIndex),
                                         m_selectedHeaderIndex + 1 >= m_currentTab->getHeaders().size() ? m_currentTab->getCount() + 1 : m_currentTab->getHeader(m_selectedHeaderIndex + 1));
          }
          index++;
        }
        ImGui::EndCombo();
      }
    } else {
      ImGui::InputText("##orpg_database_editor_search_filter", &m_searchString);
    }
    ImGui::BeginDisabled(m_currentTab->hasHeader() == 0 ? true : !m_currentTab->hasHeader());
    if (ImGui::Checkbox("By header##orpg_database_editor_filterbyheader_check", &m_filterByHeader)) {
      if (m_filterByHeader) {
        m_selectedHeaderIndex = 0;
        m_currentTab->setHeaderRange(m_currentTab->getHeader(m_selectedHeaderIndex),
                                     m_selectedHeaderIndex + 1 >= m_currentTab->getHeaders().size() ? -1 : m_currentTab->getHeader(m_selectedHeaderIndex + 1));
      } else {
        m_selectedHeaderIndex = -1;
      }
    }
    ImGui::EndDisabled();

    ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetDPIScaledValue(100));
    if (ImGui::SelectableWithBorder(trNOOP("Actors"), m_currentTab == &m_actors.value())) {
      m_currentTab = &m_actors.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Classes"), m_currentTab == &m_classes.value())) {
      m_currentTab = &m_classes.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Skills"), m_currentTab == &m_skills.value())) {
      m_currentTab = &m_skills.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Items"), m_currentTab == &m_items.value())) {
      m_currentTab = &m_items.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Weapons"), m_currentTab == &m_weapons.value())) {
      m_currentTab = &m_weapons.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Armors"), m_currentTab == &m_armors.value())) {
      m_currentTab = &m_armors.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Enemies"), m_currentTab == &m_enemies.value())) {
      m_currentTab = &m_enemies.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Troops"), m_currentTab == &m_troops.value())) {
      m_currentTab = &m_troops.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("States"), m_currentTab == &m_states.value())) {
      m_currentTab = &m_states.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Animations"), m_currentTab == &m_animations.value())) {
      m_currentTab = &m_animations.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Tilesets"), m_currentTab == &m_tilesets.value())) {
      m_currentTab = &m_tilesets.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Common Events"), m_currentTab == &m_commonEvents.value())) {
      m_currentTab = &m_commonEvents.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("System"), m_currentTab == &m_system.value())) {
      m_currentTab = &m_system.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Types"), m_currentTab == &m_types.value())) {
      m_currentTab = &m_types.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Terms"), m_currentTab == &m_terms.value())) {
      m_currentTab = &m_terms.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Exported Constants"), m_currentTab == &m_gameConstants.value())) {
      m_currentTab = &m_gameConstants.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Templates"), m_currentTab == &m_templates.value())) {
      m_currentTab = &m_templates.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Data Sorting"), m_currentTab == &m_mappings.value())) {
      m_currentTab = &m_mappings.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Documentation"), m_currentTab == &m_docs.value())) {
      m_currentTab = &m_docs.value();
    }
    if (ImGui::SelectableWithBorder(trNOOP("Localization"), m_currentTab == &m_locales.value())) {
      m_currentTab = &m_locales.value();
    }
    ImGui::EndChild();
    ImGui::SameLine();
    if (m_currentTab) {
      m_currentTab->draw();
    }
  }

  ImGui::End();
}

const IconSheet* DatabaseEditor::getIconSheet() {
  if (!m_iconSheet) {
    m_iconSheet.emplace("system/IconSet"sv);
  }
  return &m_iconSheet.value();
}


code:
#pragma once

#include "Core/DatabaseEditor/DBActorsTab.hpp"
#include "Core/DatabaseEditor/DBAnimationsTab.hpp"
#include "Core/DatabaseEditor/DBArmorsTab.hpp"
#include "Core/DatabaseEditor/DBClassesTab.hpp"
#include "Core/DatabaseEditor/DBCommonEventsTab.hpp"
#include "Core/DatabaseEditor/DBDocTab.hpp"
#include "Core/DatabaseEditor/DBEnemiesTab.hpp"
#include "Core/DatabaseEditor/DBGameConstantsTab.hpp"
#include "Core/DatabaseEditor/DBItemsTab.hpp"
#include "Core/DatabaseEditor/DBSkillsTab.hpp"
#include "Core/DatabaseEditor/DBStatesTab.hpp"
#include "Core/DatabaseEditor/DBSystemTab.hpp"
#include "Core/DatabaseEditor/DBTemplatesTab.hpp"
#include "Core/DatabaseEditor/DBTermsTab.hpp"
#include "Core/DatabaseEditor/DBTilesetsTab.hpp"
#include "Core/DatabaseEditor/DBTroopsTab.hpp"
#include "Core/DatabaseEditor/DBTypesTab.hpp"
#include "Core/DatabaseEditor/DBWeaponsTab.hpp"
#include "Core/Graphics/IconSheet.hpp"

#include "DatabaseEditor/DBLocaleTab.hpp"
#include "DatabaseEditor/DBMappingsTab.hpp"

struct MainWindow;
class DatabaseEditor {
public:
  DatabaseEditor() = delete;
  explicit DatabaseEditor(MainWindow* parent)
  : m_parent(parent) {}

  void draw();

  void open() { m_isOpen = true; }

  MainWindow* project() { return m_parent; }
  const MainWindow* project() const { return m_parent; }

  void setActors(Actors& actors) { m_actors.emplace(actors, this); }
  void setClasses(Classes& classes) { m_classes.emplace(classes, this); }
  void setSkills(Skills& skills) { m_skills.emplace(skills, this); }
  void setItems(Items& items) { m_items.emplace(items, this); }
  void setWeapons(Weapons& weapons) { m_weapons.emplace(weapons, this); }
  void setArmors(Armors& armors) { m_armors.emplace(armors, this); }
  void setEnemies(Enemies& enemies) { m_enemies.emplace(enemies, this); }
  void setTroops(Troops& troops) { m_troops.emplace(troops, this); }
  void setStates(States& states) { m_states.emplace(states, this); }
  void setAnimations(Animations& animations) { m_animations.emplace(animations, this); }
  void setTilesets(Tilesets& tilesets) { m_tilesets.emplace(tilesets, this); }
  void setCommonEvents(CommonEvents& commonEvents) { m_commonEvents.emplace(commonEvents, this); }
  void setSystem(System& system) {
    m_system.emplace(system, this);
    m_types.emplace(system, this);
    m_terms.emplace(system, this);
    m_mappings.emplace(system, this);
  }
  void setGameConstants(GameConstants& gameConstants) { m_gameConstants.emplace(gameConstants, this); }
  void setTemplates(Templates& templates) { m_templates.emplace(templates, this); }
  void setDocs(Docs& docs) { m_docs.emplace(docs, this); }
  void setLocales(Locales& locales) { m_locales.emplace(locales, this); }

  bool isReady() const {
    return m_actors && m_classes && m_skills && m_items && m_weapons && m_armors && m_enemies && m_troops && m_states && m_animations && m_tilesets && m_commonEvents && m_system && m_terms &&
           m_types && m_gameConstants && m_templates && m_docs && m_mappings && m_locales;
  }
  const IconSheet* getIconSheet();

  std::string& getFilterString() { return m_searchString; } // TODO: Search by string
  bool isFilteredByCategory() const { return m_filterByHeader && m_selectedHeaderIndex != -1; }

  bool isOpen() { return m_isOpen; }
  DBCommonEventsTab* commonEvents() { return m_commonEvents.has_value() ? &m_commonEvents.value() : nullptr; }
  IDBEditorTab* getCurrentTab() { return m_currentTab; }

  rpgmutils::signal<void()> onReady;

private:
  MainWindow* m_parent;
  std::optional<DBActorsTab> m_actors;
  std::optional<DBClassesTab> m_classes;
  std::optional<DBSkillsTab> m_skills;
  std::optional<DBItemsTab> m_items;
  std::optional<DBWeaponsTab> m_weapons;
  std::optional<DBArmorsTab> m_armors;
  std::optional<DBEnemiesTab> m_enemies;
  std::optional<DBTroopsTab> m_troops;
  std::optional<DBStatesTab> m_states;
  std::optional<DBAnimationsTab> m_animations;
  std::optional<DBTilesetsTab> m_tilesets;
  std::optional<DBCommonEventsTab> m_commonEvents;
  std::optional<DBSystemTab> m_system;
  std::optional<DBTypesTab> m_types;
  std::optional<DBTermsTab> m_terms;
  std::optional<DBGameConstantsTab> m_gameConstants;
  std::optional<DBTemplatesTab> m_templates;
  std::optional<DBDocTab> m_docs;
  std::optional<DBLocaleTab> m_locales;
  std::optional<DBMappingsTab> m_mappings;
  std::optional<IconSheet> m_iconSheet;

  std::string m_searchString;
  bool m_filterByHeader{false};
  int m_selectedHeaderIndex{-1};
  std::vector<std::string> m_headers;

  IDBEditorTab* m_currentTab = nullptr;
  bool m_isOpen{};
};


code:
#include "Core/DatabaseEditor/DBActorsTab.hpp"
#include "Database/Class.hpp"

#include "Core/CommonUI/TraitsEditor.hpp"
#include "Core/DatabaseEditor.hpp"
#include "Core/Graphics/SideViewBattlerSheet.hpp"
#include "Core/ImGuiExt/ImGuiParsedText.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"
#include "imgui.h"
#include "imgui_internal.h"

DBActorsTab::DBActorsTab(Actors& actors, DatabaseEditor* parent)
: IDBEditorTab(parent)
, m_actors(actors) {
  m_selectedActor = m_actors.actor(1);
  if (m_selectedActor) {
    m_traitsEditor.setTraits(&m_selectedActor->traits());
  }
}

void DBActorsTab::draw() {
  if (m_selectedActor != nullptr) {
    if (!m_faceSheet) {
      m_faceSheet.emplace(m_selectedActor->faceName());
    }
    if (!m_characterSheet) {
      m_characterSheet.emplace(m_selectedActor->characterName());
    }
    if (!m_battlerSheet) {
      m_battlerSheet.emplace(m_selectedActor->battlerName());
    }
  }

  if (!m_faceButton) {
    m_faceButton.emplace();
    m_faceButton->setSize(144, 144);
  }

  if (!m_actorButton) {
    m_actorButton.emplace();
    m_actorButton->setSize(144, 144);
  }
  if (!m_battlerButton) {
    m_battlerButton.emplace();
    m_battlerButton->setSize(144, 144);
  }

  if (m_selectedActor) {
    if (!m_faceButton->hasCompositeTextures()) {
      const auto [min, max] = m_faceSheet->getFaceRect(m_selectedActor->faceIndex());
      m_faceButton->setTexturesToComposite({{m_faceSheet->texture(),
                                             {m_faceSheet->faceWidth(), m_faceSheet->faceWidth()},
                                             {static_cast<int>(min.x() * m_faceSheet->texture().width()), static_cast<int>(min.y() * m_faceSheet->texture().height())}}});
    }
    if (!m_actorButton->hasCompositeTextures()) {
      const auto [min, max] = m_characterSheet->getRectForCharacter(m_selectedActor->characterIndex(), 1);
      m_actorButton->setTexturesToComposite({{m_characterSheet->texture(),
                                              {m_characterSheet->characterWidth(), m_characterSheet->characterHeight()},
                                              {static_cast<int>(min.x() * m_characterSheet->texture().width()), static_cast<int>(min.y() * m_characterSheet->texture().height())}}});
    }
    if (!m_battlerButton->hasCompositeTextures()) {
      const auto [_, frames] = m_battlerSheet->getAction(SideViewActionType::StepForward);
      m_battlerButton->setTexturesToComposite({{m_battlerSheet->texture(),
                                                {m_battlerSheet->characterWidth(), m_battlerSheet->characterHeight()},
                                                {static_cast<int>(frames[1].min.x() * m_battlerSheet->texture().width()), static_cast<int>(frames[1].min.y() * m_battlerSheet->texture().height())}}});
    }
  }

  ImGui::BeginChild("##orpg_actors_editor");
  {

    const auto calc = ImGui::CalcTextSize("ABCDEFGHIJKLMNOPQRSTUV");
    ImGui::BeginChild("##orpg_actors_editor_actors", ImVec2{calc.x + (ImGui::GetStyle().ItemSpacing.x * 2), 0}, ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX, ImGuiWindowFlags_NoScrollbar);
    {
      ImGui::BeginGroup();
      {
        ImGui::SeparatorText("Actors");
        ImGui::BeginChild("##orpg_actors_editor_actor_list", ImVec2{0, ImGui::GetContentRegionMax().y - ((calc.y + ImGui::GetStyle().ItemSpacing.y) * 4)}, ImGuiChildFlags_Borders);
        {
          ImGui::BeginGroup();
          {
            for (auto& actor : m_actors.m_actors) {
              if (actor.id() == 0) {
                continue;
              }

              if (ImGui::Selectable(std::format("{}{}", Database::instance()->actorNameAndId(actor.id()), actor.isModified() ? "*" : "").c_str(), &actor == m_selectedActor,
                                    ImGuiSelectableFlags_SelectOnNav)) {
                if (m_selectedActor != &actor) {
                  m_selectedActor = &actor;
                  m_faceSheet.emplace(m_selectedActor->faceName());
                  m_characterSheet.emplace(m_selectedActor->characterName());
                  m_battlerSheet.emplace(m_selectedActor->battlerName());
                  m_traitsEditor.setTraits(&m_selectedActor->traits());
                  m_faceButton->clear();
                  m_actorButton->clear();
                  m_battlerButton->clear();
                }
              }
            }
          }
          ImGui::EndGroup();
        }
        ImGui::EndChild();
        char str[4096];
        snprintf(str, 4096, "Max Actors %i", m_actors.count());
        ImGui::SeparatorText(str);
        if (ImGui::Button("Change Max", ImVec2{ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x, 0})) {
          m_changeIntDialogOpen = true;
          m_editMaxActors = m_actors.count();
        }
      }
      ImGui::EndGroup();
    }
    ImGui::EndChild();
    ImGui::SameLine();
    ImGui::BeginChild("##orpg_actors_editor_actors_actor_properties");
    {
      if (m_selectedActor) {
        ImGui::BeginChild("##orpg_actors_actor_panel_left", ImVec2{(ImGui::GetContentRegionAvail().x / 2) - ImGui::GetStyle().FramePadding.x, 0.f}, ImGuiChildFlags_Border | ImGuiChildFlags_ResizeX);
        {
          ImGui::BeginGroup();
          {
            ImGui::SeparatorText("General Settings");
            char name[4096];
            strncpy(name, m_selectedActor->name().c_str(), 4096);
            if (ImGui::LabelOverLineEdit("##orpg_actors_editor_actors_actor_name", "Name:", name, 4096, (ImGui::GetContentRegionAvail().x / 2) - ImGui::GetStyle().FramePadding.x)) {

              m_selectedActor->setName(name);
            }
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 4);
            strncpy(name, m_selectedActor->nickname().c_str(), 4096);
            if (ImGui::LabelOverLineEdit("##orpg_actors_editor_actors_actor_nickname", "Nickname:", name, 4096, (ImGui::GetContentRegionAvail().x / 2) - ImGui::GetStyle().FramePadding.x)) {

              m_selectedActor->setNickname(name);
            }
            ImGui::BeginGroup();
            {
              ImGui::Text("Class:");
              const auto cls = Database::instance()->classes.classType(m_selectedActor->classId());
              ImGui::SetNextItemWidth((ImGui::GetContentRegionAvail().x / 2) - ImGui::GetStyle().FramePadding.x);
              if (ImGui::BeginCombo("##orpg_actors_editor_class_combo", Database::instance()->classNameAndId(cls->id()).c_str())) {
                for (const auto& c : Database::instance()->classes.classes()) {
                  if (c.id() == 0) {
                    continue;
                  }
                  if (ImGui::Selectable(Database::instance()->classNameAndId(c.id()).c_str(), c.id() == m_selectedActor->classId())) {

                    m_selectedActor->setClassId(c.id());
                    m_selectedActor->setValid(true);
                  }
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetStyle().ItemSpacing.y);
            ImGui::BeginGroup();
            {
              ImGui::Text("Initial Level:");
              ImGui::SetNextItemWidth((ImGui::GetContentRegionAvail().x / 2) - ImGui::GetStyle().FramePadding.x);
              int initialLevel = m_selectedActor->initialLevel();
              ImGui::InputInt("##orpg_actors_initial_level_edit", &initialLevel);
              if (ImGui::IsItemDeactivatedAfterEdit()) {
                m_selectedActor->setInitialLevel(std::clamp(initialLevel, 1, 99));
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetStyle().ItemSpacing.y);
            ImGui::BeginGroup();
            {
              ImGui::Text("Max Level:");
              ImGui::SetNextItemWidth((ImGui::GetContentRegionMax().x / 2 / 2) - ImGui::GetStyle().FramePadding.x);
              int maxLevel = m_selectedActor->maxLevel();
              ImGui::InputInt("##orpg_actors_max_level_edit", &maxLevel);
              if (ImGui::IsItemDeactivatedAfterEdit()) {
                m_selectedActor->setMaxLevel(std::clamp(maxLevel, 1, 99));
              }
            }
            ImGui::EndGroup();
            ImGui::BeginGroup();
            {
              ImGui::Text("Profile:");
              char profile[8192];
              strncpy(profile, m_selectedActor->profile().c_str(), IM_ARRAYSIZE(profile));
              if (ImGui::InputTextMultiline("##orpg_actors_profile", profile, IM_ARRAYSIZE(profile), ImVec2{ImGui::GetContentRegionMax().x - 16, 0})) {
                m_selectedActor->setProfile(profile);
              }
            }
            ImGui::EndGroup();
          }
          ImGui::EndGroup();
          ImGui::BeginChild("##orpg_actors_editor_actor_images", {}, ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_HorizontalScrollbar);
          {
            ImGui::SeparatorText("Images");
            ImGui::BeginGroup();
            {
              ImGui::Text("Face:");
              if (ImGui::ImageButtonEx(ImGui::GetID("##orpg_actors_face_image"), m_faceButton->get(), ImVec2{m_faceButton->size()}, {0.f, 0.f}, {1.f, 1.f}, {}, {1.f, 1.f, 1.f, 1.f},
                                       ImGuiButtonFlags_PressedOnDoubleClick)) {
                if (!m_facePicker) {
                  m_facePicker.emplace(m_selectedActor->faceName(), m_selectedActor->faceIndex());
                } else {
                  m_facePicker->setFaceInfo(m_selectedActor->faceName(), m_selectedActor->faceIndex());
                }
                m_facePicker->setOpen(true);
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::BeginGroup();
            {
              ImGui::Text("Character:");
              if (ImGui::ImageButtonEx(ImGui::GetID("##orpg_actors_character_image"), m_actorButton->get(), ImVec2{m_actorButton->size()}, {0.f, 0.f}, {1.f, 1.f}, {}, {1.f, 1.f, 1.f, 1.f},
                                       ImGuiButtonFlags_PressedOnDoubleClick)) {
                if (!m_actorPicker) {
                  m_actorPicker.emplace(CharacterPicker::PickerMode::Character);
                }
                m_actorPicker->setCharacterInfo(m_selectedActor->characterName(), m_selectedActor->characterIndex());
                m_actorPicker->setOpen(true);
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::BeginGroup();
            {
              ImGui::Text("[SV] Battler:");
              if (ImGui::ImageButtonEx(ImGui::GetID("##orpg_actors_battler_image"), m_battlerButton->get(), ImVec2{m_battlerButton->size()}, {0.f, 0.f}, {1.f, 1.f}, {}, {1.f, 1.f, 1.f, 1.f},
                                       ImGuiButtonFlags_PressedOnDoubleClick)) {
                if (!m_battlerPicker) {
                  m_battlerPicker.emplace(ImagePicker::PickerMode::SVBattler, m_selectedActor->battlerName());
                }
                m_battlerPicker->setImageInfo(m_selectedActor->battlerName());
                m_battlerPicker->setOpen(true);
              }
            }
            ImGui::EndGroup();
          }
          ImGui::EndChild();
          ImGui::BeginGroup();
          {
            ImGui::SeparatorText("Initial Equipment");
            if (ImGui::BeginTable("##orpg_actors_actor_init_equip", 2,
                                  ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY,
                                  ImVec2{ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x, ImGui::GetContentRegionAvail().y - ImGui::GetStyle().FramePadding.y})) {

              ImGui::TableSetupColumn("Type");
              ImGui::TableSetupColumn("Equipment Item");
              ImGui::TableHeadersRow();
              const auto& equipTypes = Database::instance()->system.equipTypes();

              for (int i = 0; i < equipTypes.size() - 1; i++) {
                ImGui::TableNextRow();
                ImGui::TableNextColumn();
                const int etypeId = Database::instance()->slotIdToEquipId(m_selectedActor->id(), i);
                auto etypeName = equipTypes[etypeId];
                int dataId = i < m_selectedActor->equips().size() ? m_selectedActor->equips()[i] : 0;
                if (!Database::instance()->checkEquipable(m_selectedActor->id(), etypeId, dataId)) {
                  dataId = 0;
                }

                if (ImGui::Selectable(std::format("{}##{}", etypeName, i).c_str(), m_selectedEquip == i, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
                  m_selectedEquip = i;
                  if (ImGui::GetMouseClickedCount(ImGuiMouseButton_Left) >= 2) {
                    m_showEquipEdit = true;
                    m_chosenDataId = dataId;
                    m_chosenEquipId = etypeId;
                  }
                }

                ImGui::TableNextColumn();
                ImGui::TextUnformatted(Database::instance()->itemDisplayName(etypeId <= 1, dataId).c_str());
              }
              ImGui::EndTable();
            }
          }
          ImGui::EndGroup();
        }
        ImGui::EndChild();
        ImGui::SameLine();
        ImGui::BeginChild("##orpg_actors_actors_panel_right");
        { m_traitsEditor.draw(m_parent); }
        ImGui::EndChild();
        ImGui::BeginChild("##orpg_actors_actors_panel_bottomright");
        {
          ImGui::SeparatorText("Note:");
          char note[8192];
          strncpy(note, m_selectedActor->note().c_str(), IM_ARRAYSIZE(note));
          if (ImGui::InputTextMultiline(std::format("##orpg_database_actor_note{}", m_selectedActor->id()).c_str(), note, IM_ARRAYSIZE(note), ImVec2{ImGui::GetContentRegionMax().x - 16, 400})) {
            m_selectedActor->setNote(note);
          }
        }
        ImGui::EndChild();
      }
    }
    ImGui::EndChild();
  }
  ImGui::EndChild();

  if (m_changeIntDialogOpen) {
    if (ImGui::Begin("Change Max Actors", &m_changeIntDialogOpen,
                     ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_Modal | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) {
      ImGui::Text(
          "Specify an amount to resize the actor list to\n"
          "This cannot be undone!");
      ImGui::InputInt("##value_input", &m_editMaxActors);
      ImGui::SameLine();
      if (ImGui::Button("Accept")) {
        m_changeConfirmDialogOpen = true;
      }
      ImGui::SameLine();
      if (ImGui::Button("Cancel")) {
        m_changeIntDialogOpen = false;
      }
    }
    ImGui::End();

    if (m_changeConfirmDialogOpen) {
      if (ImGui::Begin("Confirm Change", &m_changeConfirmDialogOpen,
                       ImGuiWindowFlags_NoResize | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) {
        ImGui::Text("Are you sure?");
        if (ImGui::Button("Yes")) {
          const int tmpId = m_selectedActor->id();
          m_actors.resize(m_editMaxActors);
          m_selectedActor = m_actors.actor(tmpId);
          m_changeIntDialogOpen = false;
          m_changeConfirmDialogOpen = false;
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) {
          m_changeIntDialogOpen = false;
          m_changeConfirmDialogOpen = false;
        }
      }
      ImGui::End();
    }
  }

  if (m_showEquipEdit) {
    if (ImGui::Begin("Select Equipment...", &m_showEquipEdit, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse)) {
      ImGui::TextUnformatted(m_chosenEquipId <= 1 ? trNOOP("Weapon:") : trNOOP("Armor:"));

      std::vector<int> equipList;

      if (!Database::instance()->isEquipTypeSealed(m_selectedActor->id(), m_chosenEquipId)) {
        if (m_chosenEquipId <= 1) {
          for (int i = 0; const auto& wp : Database::instance()->weapons.weapons()) {
            if (Database::instance()->isEquipWeaponTypeOk(m_selectedActor->id(), wp.wtypeId())) {
              equipList.push_back(i);
            }
            ++i;
          }
        } else {
          for (int i = 0; const auto& ar : Database::instance()->armors.armors()) {
            if (m_chosenEquipId == ar.etypeId() && Database::instance()->isEquipArmorTypeOk(m_selectedActor->id(), ar.atypeId())) {
              equipList.push_back(i);
            }
            ++i;
          }
        }
      }

      const auto preview = m_chosenDataId == 0 ? "None" : Database::instance()->itemDisplayName(m_chosenEquipId <= 1, m_chosenDataId);
      if (ImGui::BeginCombo("##equip_combo", preview.c_str())) {
        for (const auto& equip : equipList) {
          if (ImGui::Selectable(std::format("{}##_{}", Database::instance()->itemDisplayName(m_chosenEquipId <= 1, equip), equip).c_str(), m_chosenDataId == equip)) {
            m_chosenDataId = equip;
          }
        }
        ImGui::EndCombo();
      }

      if (ImGui::Button("OK")) {
        m_showEquipEdit = false;
        m_selectedActor->setEquip(m_selectedEquip, m_chosenDataId);
      }
      ImGui::SameLine();
      if (ImGui::Button("Cancel")) {
        m_showEquipEdit = false;
      }
    }
    ImGui::End();
  }

  if (m_actorPicker) {
    if (const auto& [closed, confirmed] = m_actorPicker->draw(); closed) {
      if (confirmed) {
        m_selectedActor->setCharacterName(m_actorPicker->selectedSheet());
        m_selectedActor->setCharacterIndex(m_actorPicker->character());
        m_actorButton.reset();
        m_characterSheet.reset();
        m_actorPicker->accept();
      }
    }
  }

  if (m_battlerPicker) {
    if (const auto& [closed, confirmed] = m_battlerPicker->draw(); closed) {
      if (confirmed) {
        m_selectedActor->setBattlerName(m_battlerPicker->selectedImage());
        m_battlerButton.reset();
        m_battlerSheet.reset();
        m_battlerPicker->accept();
      }
    }
  }
  if (m_facePicker) {
    if (const auto& [closed, confirmed] = m_facePicker->draw(); closed) {
      if (confirmed) {
        m_selectedActor->setFaceName(m_facePicker->selectedSheet());
        m_selectedActor->setFaceIndex(m_facePicker->faceIndex());
        m_faceButton.reset();
        m_faceSheet.reset();
        m_facePicker->accept();
      }
    }
  }
}


code:
#pragma once
#include "Core/CommonUI/FacePicker.hpp"
#include "Core/CommonUI/TraitsEditor.hpp"
#include "Core/DatabaseEditor/IDBEditorTab.hpp"
#include "Core/Graphics/CharacterSheet.hpp"
#include "Core/Graphics/CheckeredCompositeTexture.hpp"
#include "Core/Graphics/FaceSheet.hpp"
#include "Core/Graphics/SideViewBattlerSheet.hpp"
#include "DBCommonEventsTab.hpp"

#include "Database/Actors.hpp"

#include <optional>

struct DBActorsTab : IDBEditorTab {
  DBActorsTab() = delete;
  explicit DBActorsTab(Actors& actors, DatabaseEditor* parent);
  void draw() override;

  Actors& actors() { return m_actors; }
  const Actors& actors() const { return m_actors; }

  Actor* actor(int id) { return m_actors.actor(id); }
  const Actor* actor(int id) const { return m_actors.actor(id); }

  bool isModified() const { return m_actors.isModified(); }

  std::vector<int>& getHeaders() override { return m_headers; }
  int getHeader(int index) override { return m_headers.at(index); }
  bool hasHeader() override { return !m_headers.empty(); }
  void setHeaderRange(int start, int end) override {
    m_categoryStart = start;
    m_categoryEnd = end;
  }
  std::string getName(int index) override { return m_actors.actor(index)->name(); }
  int getCount() override { return m_actors.count(); }

private:
  int m_categoryStart;
  int m_categoryEnd;

  std::vector<int> m_headers;
  Actors& m_actors;
  Actor* m_selectedActor{};
  int m_editMaxActors{};
  float m_splitterWidth = 300.f;
  bool m_changeIntDialogOpen = false;
  bool m_changeConfirmDialogOpen = false;
  bool m_showEquipEdit = false;
  int m_selectedEquip = 0;
  int m_chosenDataId = 0;
  int m_chosenEquipId = 0;
  std::optional<FaceSheet> m_faceSheet;
  std::optional<CharacterSheet> m_characterSheet;
  std::optional<SideViewBattlerSheet> m_battlerSheet;
  std::optional<CheckeredCompositeTexture> m_faceButton;
  std::optional<CheckeredCompositeTexture> m_actorButton;
  std::optional<CharacterPicker> m_actorPicker;
  std::optional<CheckeredCompositeTexture> m_battlerButton;
  std::optional<ImagePicker> m_battlerPicker;
  std::optional<FacePicker> m_facePicker;
  TraitsEditor m_traitsEditor;
};