WoW Model Viewer
Your premiere tool for viewing, equipping and animating World of Warcraft models.
Loading...
Searching...
No Matches
FileBrowserPanel.cpp
Go to the documentation of this file.
1#include "FileBrowserPanel.h"
2
3#include "imgui.h"
4
5#include <string>
6#include <map>
7#include <deque>
8#include <format>
9#include <algorithm>
10#include <functional>
11
12#include "Logger.h"
13#include "Game.h"
14#include "WoWFolder.h"
15#include "string_utils.h"
16
17// ---- Internal types -------------------------------------------------------
18namespace
19{
20
21struct FileBrowserNode
22{
23 std::string name;
24 GameFile* file = nullptr;
25 std::map<std::string, FileBrowserNode*> children;
26};
27
28// Arena allocator: all tree nodes live in a deque and are freed in bulk.
29// std::deque never moves existing elements on push_back, so FileBrowserNode*
30// pointers stored in children maps remain valid as the pool grows.
31std::deque<FileBrowserNode> s_nodePool;
32
33FileBrowserNode* allocNode()
34{
35 s_nodePool.emplace_back();
36 return &s_nodePool.back();
37}
38
39void freeNodePool()
40{
41 s_nodePool.clear();
42}
43
44// ---- Filter data ----------------------------------------------------------
45const char* s_filterLabels[] = {
46 "Models (*.m2)",
47 "WMOs (*.wmo)",
48 "ADTs (*.adt)",
49 "Sounds (*.wav)",
50 "OGGs (*.ogg)",
51 "MP3s (*.mp3)",
52 "Images (*.blp)",
53 "Shaders (*.bls)",
54 "DBCs (*.dbc)",
55 "DB2s (*.db2)",
56 "LUAs (*.lua)",
57 "XMLs (*.xml)",
58 "SKINs (*.skin)"
59};
60
61const char* s_filterExtensions[] = {
62 "m2", "wmo", "adt", "wav", "ogg", "mp3",
63 "blp", "bls", "dbc", "db2", "lua", "xml", "skin"
64};
65
66constexpr int s_filterCount = sizeof(s_filterLabels) / sizeof(s_filterLabels[0]);
67
68// ---- Panel state ----------------------------------------------------------
69int s_filterMode = 0;
70char s_searchBuf[256] = {};
71FileBrowserNode* s_fileTreeRoot = nullptr;
72bool s_fileTreeDirty = true;
73int s_fileTreeFileCount = 0;
74
75// ---- Tree construction ----------------------------------------------------
76void rebuildFileTree()
77{
78 freeNodePool();
79 s_fileTreeRoot = allocNode();
80 s_fileTreeRoot->name = "Root";
81
82 std::string search = core::toLower(std::string(s_searchBuf));
83 auto s = search.find_first_not_of(" \t\r\n");
84 auto e = search.find_last_not_of(" \t\r\n");
85 search = (s == std::string::npos) ? "" : search.substr(s, e - s + 1);
86
87 const std::string ext = std::string(".") + s_filterExtensions[s_filterMode];
88
89 s_fileTreeFileCount = 0;
90 for (auto* gf : GAMEDIRECTORY)
91 {
92 const auto& fname = gf->fullname();
93 if (!core::endsWithIgnoreCase(fname, ext))
94 continue;
95 if (!search.empty() && !core::containsIgnoreCase(fname, search))
96 continue;
97
98 ++s_fileTreeFileCount;
99
100 std::string displayName = std::format("{} [{}]", fname, gf->fileDataId());
101 std::transform(displayName.begin(), displayName.end(), displayName.begin(),
102 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
103 std::replace(displayName.begin(), displayName.end(), '/', '\\');
104 if (!displayName.empty())
105 displayName[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(displayName[0])));
106
107 auto parts = core::split(displayName, '\\');
108 FileBrowserNode* cur = s_fileTreeRoot;
109
110 for (int i = 0; i < static_cast<int>(parts.size()) - 1; ++i)
111 {
112 auto it = cur->children.find(parts[i]);
113 if (it == cur->children.end())
114 {
115 auto* child = allocNode();
116 child->name = parts[i];
117 cur->children[parts[i]] = child;
118 cur = child;
119 }
120 else
121 {
122 cur = it->second;
123 }
124 }
125
126 auto* leaf = allocNode();
127 leaf->name = parts.back();
128 leaf->file = gf;
129 cur->children[parts.back()] = leaf;
130 }
131
132 s_fileTreeDirty = false;
133 LOG_INFO << "File tree rebuilt: " << s_fileTreeFileCount << " files matching filter.";
134}
135
136} // anonymous namespace
137
138// ---- Public API -----------------------------------------------------------
140{
141
142GameFile* draw(bool& visible, const LoadState& load)
143{
144 GameFile* selected = nullptr;
145
146 if (ImGui::Begin("File Browser", &visible))
147 {
148 if (!load.isLoaded)
149 {
150 if (load.inProgress)
151 {
152 ImGui::Text("Loading...");
153 ImGui::ProgressBar(load.progress, ImVec2(-1, 0));
154 ImGui::TextWrapped("%s", load.statusText);
155 }
156 else
157 {
158 if (load.statusText && load.statusText[0] != '\0')
159 ImGui::TextWrapped("%s", load.statusText);
160 else
161 ImGui::TextWrapped("Game not loaded. Use Settings panel to set the WoW path and click Load WoW.");
162 }
163 }
164 else
165 {
166 // ---- Filter options ----
167 ImGui::Text("Filter:");
168 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
169 if (ImGui::Combo("##filter", &s_filterMode, s_filterLabels, s_filterCount))
170 s_fileTreeDirty = true;
171
172 ImGui::Text("Search:");
173 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
174 if (ImGui::InputText("##search", s_searchBuf, sizeof(s_searchBuf),
175 ImGuiInputTextFlags_EnterReturnsTrue))
176 s_fileTreeDirty = true;
177 ImGui::SameLine();
178 if (ImGui::Button("Apply", ImVec2(-1, 0)))
179 s_fileTreeDirty = true;
180
181 if (s_fileTreeDirty)
182 rebuildFileTree();
183
184 ImGui::Separator();
185 ImGui::Text("Files: %d", s_fileTreeFileCount);
186 ImGui::Separator();
187
188 // ---- File tree ----
189 ImGui::BeginChild("FileTree", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar);
190 if (s_fileTreeRoot)
191 {
192 std::function<void(FileBrowserNode*)> drawNode = [&](FileBrowserNode* node)
193 {
194 for (auto& [name, child] : node->children)
195 {
196 if (child->file)
197 {
198 ImGuiTreeNodeFlags leafFlags =
199 ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
200 ImGuiTreeNodeFlags_SpanAvailWidth;
201 ImGui::TreeNodeEx(child->name.c_str(), leafFlags);
202 if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen())
203 selected = child->file;
204 }
205 else
206 {
207 bool open = ImGui::TreeNodeEx(child->name.c_str(),
208 ImGuiTreeNodeFlags_SpanAvailWidth);
209 if (open)
210 {
211 drawNode(child);
212 ImGui::TreePop();
213 }
214 }
215 }
216 };
217 drawNode(s_fileTreeRoot);
218 }
219 ImGui::EndChild();
220 }
221 }
222 ImGui::End();
223
224 return selected;
225}
226
228{
229 s_fileTreeDirty = true;
230}
231
233{
234 freeNodePool();
235 s_fileTreeRoot = nullptr;
236}
237
238} // namespace FileBrowserPanel
#define GAMEDIRECTORY
Definition Game.h:9
#define LOG_INFO
Definition Logger.h:10
Abstract base class representing a file within the game data archive.
Definition GameFile.h:12
GameFile * draw(bool &visible, const LoadState &load)
void shutdown()
Free all internal allocations (call at application shutdown).
void markDirty()
Mark the file tree as dirty so it will be rebuilt on the next draw().
std::vector< std::string > split(const std::string &s, char delimiter)
Split a string by a single-character delimiter.
bool containsIgnoreCase(const std::string &s, const std::string &substr)
bool endsWithIgnoreCase(const std::string &s, const std::string &suffix)
std::string toLower(const std::string &s)