/* * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved. */ #include #include #include #include "tilesheeteditor-imgui.hpp" namespace nostalgia::core { TileSheetEditorImGui::TileSheetEditorImGui(Context *ctx, const ox::String &path): m_tileSheetEditor(ctx, path) { m_ctx = ctx; m_itemPath = path; const auto lastSlash = std::find(m_itemPath.rbegin(), m_itemPath.rend(), '/').offset(); m_itemName = m_itemPath.substr(lastSlash + 1); // init palette idx const auto &palPath = model()->palPath(); auto sctx = applicationData(m_ctx); const auto &palList = sctx->project->fileList(core::FileExt_npal + 1); for (std::size_t i = 0; const auto &pal : palList) { if (palPath == pal) { m_selectedPaletteIdx = i; break; } ++i; } // connect signal/slots undoStack()->changeTriggered.connect(this, &TileSheetEditorImGui::markUnsavedChanges); m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet); } const ox::String &TileSheetEditorImGui::itemName() const noexcept { return m_itemPath; } const ox::String &TileSheetEditorImGui::itemDisplayName() const noexcept { return m_itemName; } void TileSheetEditorImGui::exportFile() { } void TileSheetEditorImGui::cut() { model()->cut(); } void TileSheetEditorImGui::copy() { model()->copy(); } void TileSheetEditorImGui::paste() { model()->paste(); } void TileSheetEditorImGui::keyStateChanged(core::Key key, bool down) { if (!down) { return; } const auto colorCnt = model()->pal().colors.size(); if (key >= core::Key::Num_1 && key <= core::Key::Num_0 + colorCnt) { auto idx = ox::min(static_cast(key - core::Key::Num_1), colorCnt - 1); m_tileSheetEditor.setPalIdx(idx); } else if (key == core::Key::Num_0 && colorCnt >= 10) { auto idx = ox::min(static_cast(key - core::Key::Num_1 + 9), colorCnt - 1); m_tileSheetEditor.setPalIdx(idx); } } void TileSheetEditorImGui::draw(core::Context*) noexcept { const auto paneSize = ImGui::GetContentRegionAvail(); const auto tileSheetParentSize = ImVec2(paneSize.x - m_palViewWidth, paneSize.y); const auto fbSize = geo::Vec2(tileSheetParentSize.x - 16, tileSheetParentSize.y - 16); ImGui::BeginChild("TileSheetView", tileSheetParentSize, true); { drawTileSheet(fbSize); } ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("Controls", ImVec2(m_palViewWidth - 8, paneSize.y), true); { ImGui::BeginChild("ToolBox", ImVec2(m_palViewWidth - 24, 30), true); { const auto btnSz = ImVec2(45, 14); if (ImGui::Selectable("Draw", m_tool == Tool::Draw, 0, btnSz)) { m_tool = Tool::Draw; model()->clearSelection(); } ImGui::SameLine(); if (ImGui::Selectable("Select", m_tool == Tool::Select, 0, btnSz)) { m_tool = Tool::Select; } ImGui::SameLine(); if (ImGui::Selectable("Fill", m_tool == Tool::Fill, 0, btnSz)) { m_tool = Tool::Fill; model()->clearSelection(); } } ImGui::EndChild(); const auto ySize = paneSize.y - 36; // draw palette/color picker ImGui::BeginChild("Palette", ImVec2(m_palViewWidth - 24, ySize / 2.07f), true); { drawPaletteSelector(); } ImGui::EndChild(); ImGui::BeginChild("SubSheets", ImVec2(m_palViewWidth - 24, ySize / 2.03f), true); { static constexpr auto btnHeight = 18; const auto btnSize = ImVec2(18, btnHeight); if (ImGui::Button("+", btnSize)) { auto insertOnIdx = model()->activeSubSheetIdx(); const auto &parent = *model()->activeSubSheet(); model()->addSubsheet(insertOnIdx); insertOnIdx.emplace_back(parent.subsheets.size() - 1); model()->setActiveSubsheet(insertOnIdx); } ImGui::SameLine(); if (ImGui::Button("-", btnSize)) { const auto &activeSubsheetIdx = model()->activeSubSheetIdx(); if (activeSubsheetIdx.size() > 0) { model()->rmSubsheet(activeSubsheetIdx); } } ImGui::SameLine(); if (ImGui::Button("Edit", ImVec2(36, btnHeight))) { showSubsheetEditor(); } TileSheet::SubSheetIdx path; static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; if (ImGui::BeginTable("Subsheets", 3, flags)) { ImGui::TableSetupColumn("Subsheet", ImGuiTableColumnFlags_NoHide); ImGui::TableSetupColumn("Columns", ImGuiTableColumnFlags_WidthFixed, 50); ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthFixed, 50); ImGui::TableHeadersRow(); drawSubsheetSelector(&m_tileSheetEditor.img().subsheet, &path); ImGui::EndTable(); } } ImGui::EndChild(); } ImGui::EndChild(); m_subsheetEditor.draw(); } void TileSheetEditorImGui::drawSubsheetSelector(TileSheet::SubSheet *subsheet, TileSheet::SubSheetIdx *path) { ImGui::TableNextRow(0, 5); using Str = ox::BasicString<100>; auto pathStr = ox::join("##", *path).value; auto lbl = ox::sfmt("{}##{}", subsheet->name, pathStr); const auto rowSelected = *path == model()->activeSubSheetIdx(); const auto flags = ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_DefaultOpen | (subsheet->subsheets.size() ? 0 : ImGuiTreeNodeFlags_Leaf) | (rowSelected ? ImGuiTreeNodeFlags_Selected : 0); ImGui::TableNextColumn(); const auto open = ImGui::TreeNodeEx(lbl.c_str(), flags); Str newName = subsheet->name.c_str(); ImGui::SameLine(); if (ImGui::IsItemClicked()) { model()->setActiveSubsheet(*path); } if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) { showSubsheetEditor(); } if (subsheet->subsheets.size()) { ImGui::TableNextColumn(); ImGui::Text("--"); ImGui::TableNextColumn(); ImGui::Text("--"); } else { ImGui::TableNextColumn(); ImGui::Text("%d", subsheet->columns); ImGui::TableNextColumn(); ImGui::Text("%d", subsheet->rows); } if (open) { for (auto i = 0ul; auto &child : subsheet->subsheets) { path->push_back(i); ImGui::PushID(static_cast(i)); drawSubsheetSelector(&child, path); ImGui::PopID(); path->pop_back(); ++i; } ImGui::TreePop(); } } studio::UndoStack *TileSheetEditorImGui::undoStack() noexcept { return model()->undoStack(); } [[nodiscard]] geo::Vec2 TileSheetEditorImGui::clickPos(const ImVec2 &winPos, geo::Vec2 clickPos) noexcept { clickPos.x -= winPos.x + 10; clickPos.y -= winPos.y + 10; return clickPos; } ox::Error TileSheetEditorImGui::saveItem() noexcept { return model()->saveFile(); } void TileSheetEditorImGui::showSubsheetEditor() noexcept { const auto sheet = model()->activeSubSheet(); if (sheet->subsheets.size()) { m_subsheetEditor.show(sheet->name, -1, -1); } else { m_subsheetEditor.show(sheet->name, sheet->columns, sheet->rows); } } void TileSheetEditorImGui::drawTileSheet(const geo::Vec2 &fbSize) noexcept { const auto winPos = ImGui::GetWindowPos(); const auto fbSizei = geo::Size(static_cast(fbSize.x), static_cast(fbSize.y)); if (m_framebuffer.width != fbSizei.width || m_framebuffer.height != fbSizei.height) { m_framebuffer = glutils::generateFrameBuffer(fbSizei.width, fbSizei.height); m_tileSheetEditor.resizeView(fbSize); } else if (m_tileSheetEditor.updated()) { m_tileSheetEditor.resizeView(fbSize); m_tileSheetEditor.ackUpdate(); } glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer); // clear screen and draw glViewport(0, 0, fbSizei.width, fbSizei.height); m_tileSheetEditor.draw(); glBindFramebuffer(GL_FRAMEBUFFER, 0); ImGui::Image( reinterpret_cast(m_framebuffer.color.id), static_cast(fbSize), ImVec2(0, 1), ImVec2(1, 0)); // handle input, this must come after drawing const auto &io = ImGui::GetIO(); const auto mousePos = geo::Vec2(io.MousePos); if (ImGui::IsItemHovered()) { const auto wheel = io.MouseWheel; const auto wheelh = io.MouseWheelH; if (wheel != 0) { const auto zoomMod = ox::defines::OS == ox::OS::Darwin ? io.KeySuper : core::buttonDown(m_ctx, core::Key::Mod_Ctrl); m_tileSheetEditor.scrollV(fbSize, wheel, zoomMod); } if (wheelh != 0) { m_tileSheetEditor.scrollH(fbSize, wheelh); } if (io.MouseDown[0] && m_prevMouseDownPos != mousePos) { m_prevMouseDownPos = mousePos; switch (m_tool) { case Tool::Draw: m_tileSheetEditor.clickDraw(fbSize, clickPos(winPos, mousePos)); break; case Tool::Fill: m_tileSheetEditor.clickFill(fbSize, clickPos(winPos, mousePos)); break; case Tool::Select: m_tileSheetEditor.clickSelect(fbSize, clickPos(winPos, mousePos)); break; case Tool::None: break; } } } if (ImGui::BeginPopupContextItem("TileMenu", ImGuiPopupFlags_MouseButtonRight)) { const auto popupPos = geo::Vec2(ImGui::GetWindowPos()); if (ImGui::MenuItem("Insert Tile")) { m_tileSheetEditor.insertTile(fbSize, clickPos(winPos, popupPos)); } if (ImGui::MenuItem("Delete Tile")) { m_tileSheetEditor.deleteTile(fbSize, clickPos(winPos, popupPos)); } ImGui::EndPopup(); } if (io.MouseReleased[0]) { m_prevMouseDownPos = {-1, -1}; m_tileSheetEditor.releaseMouseButton(); } } void TileSheetEditorImGui::drawPaletteSelector() noexcept { auto sctx = applicationData(m_ctx); const auto &files = sctx->project->fileList(core::FileExt_npal + 1); const auto first = m_selectedPaletteIdx < files.size() ? files[m_selectedPaletteIdx].c_str() : ""; if (ImGui::BeginCombo("Palette", first, 0)) { for (auto n = 0u; n < files.size(); n++) { const auto selected = (m_selectedPaletteIdx == n); if (ImGui::Selectable(files[n].c_str(), selected) && m_selectedPaletteIdx != n) { m_selectedPaletteIdx = n; oxLogError(model()->setPalette(files[n])); } if (selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } // header if (ImGui::BeginTable("PaletteTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("No.", 0, 0.45); ImGui::TableSetupColumn("", 0, 0.22); ImGui::TableSetupColumn("Color16", 0, 3); ImGui::TableHeadersRow(); for (auto i = 0u; auto c: m_tileSheetEditor.pal().colors) { ImGui::PushID(static_cast(i)); // Column: color idx ImGui::TableNextColumn(); const auto label = ox::BString<8>() + (i + 1); const auto rowSelected = i == m_tileSheetEditor.palIdx(); if (ImGui::Selectable(label.c_str(), rowSelected, ImGuiSelectableFlags_SpanAllColumns)) { m_tileSheetEditor.setPalIdx(i); } // Column: color RGB ImGui::TableNextColumn(); auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1)); ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic); ImGui::TableNextColumn(); ImGui::Text("(%02d, %02d, %02d)", red16(c), green16(c), blue16(c)); ImGui::TableNextRow(); ImGui::PopID(); ++i; } ImGui::EndTable(); } } ox::Error TileSheetEditorImGui::updateActiveSubsheet(const ox::String &name, int cols, int rows) noexcept { return model()->updateSubsheet(model()->activeSubSheetIdx(), name, cols, rows); } ox::Error TileSheetEditorImGui::markUnsavedChanges(const studio::UndoCommand*) noexcept { setUnsavedChanges(true); return OxError(0); } void TileSheetEditorImGui::SubSheetEditor::draw() noexcept { constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; constexpr auto popupName = "Edit Subsheet"; if (!m_show) { return; } ImGui::OpenPopup(popupName); const auto modSize = m_cols > 0; const auto popupHeight = modSize ? 125.f : 80.f; ImGui::SetNextWindowSize(ImVec2(235, popupHeight)); if (ImGui::BeginPopupModal(popupName, &m_show, modalFlags)) { ImGui::InputText("Name", m_name.data(), m_name.cap()); if (modSize) { ImGui::InputInt("Columns", &m_cols); ImGui::InputInt("Rows", &m_rows); } if (ImGui::Button("OK")) { ImGui::CloseCurrentPopup(); m_show = false; inputSubmitted.emit(m_name.c_str(), m_cols, m_rows); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { ImGui::CloseCurrentPopup(); m_show = false; } ImGui::EndPopup(); } } }