RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
DirectoryWindow.h
Go to the documentation of this file.
1#pragma once
2#include <GL/glew.h>
3#include "imgui.h"
4#include <iostream>
5#include <vector>
6#include <filesystem>
7#include <string>
8#include <thread>
9#include <shared_mutex>
10#include <condition_variable>
11#include "Texture.h"
12//#define STB_IMAGE_IMPLEMENTATION
13#include "stb_image.h"
14
15enum class DirectoryKind
16{
17 MARKER = 1,
18 MAP = 2
19};
20
22{
23public:
25 {
26 this->directoryPath = directoryPath;
27 this->directoryName = directoryName;
28 this->kind_ = kind_;
29 }
30
31 struct ImageData
32 {
33 GLuint textureID;
34 glm::vec2 size;
35 std::string filename;
36 };
37
38 // Function to truncate a string if it exceeds a certain length
39 std::string TruncateString(const std::string& str, size_t maxLength)
40 {
41 if (str.length() > maxLength)
42 {
43 return str.substr(0, maxLength) + "...";
44 }
45 return str;
46 }
47
48 // Function to generate the texture IDs
50 {
51 // Wait for the monitoring thread to finish one full loop
52 //{
53 // std::shared_lock<std::shared_mutex> lock(imagesMutex); // Shared lock: waits if the thread is still writing
54 // if (!first_scan_done) {
55 // lock.unlock();
56 // std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Wait and retry
57 // generateTextureIDs(); // Retry generating textures
58 // return;
59 // }
60 //}
61
62 while (true)
63 {
64 {
65 std::shared_lock<std::shared_mutex> lock(imagesMutex);
67 break;
68 }
69 std::this_thread::sleep_for(std::chrono::milliseconds(100));
70 }
71
72 // Now generate textures
73 std::unique_lock<std::shared_mutex> lock(imagesMutex); // Unique lock to write (generate textures)
74 for (auto& image : images)
75 {
76 if (image.textureID == 0)
77 {
78 std::string path_file = directoryPath + "\\" + image.filename;
79 image = LoadTextureFromFile(path_file.c_str());
80 }
81 }
82 }
83
85 {
86 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoCollapse;
88 {
89 window_flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize;
90 }
91
92 ImGui::SetNextWindowSizeConstraints(ImVec2(ImGui::GetIO().DisplaySize.x * 0.1, ImGui::GetIO().DisplaySize.y * 0.1), ImVec2(ImGui::GetIO().DisplaySize.x - 200, ImGui::GetIO().DisplaySize.y));
93 ImGui::Begin(directoryName.c_str(), NULL, window_flags);
94 ImGui::Text("Path: %s", directoryPath.c_str());
95
96 // --- Fixed header (no scrolling) ---
98 {
99 float minScale = 0.10f, maxScale = 10.0f;
100 ImGui::Separator();
101 ImGui::TextUnformatted("Default Marker Size Scale");
102 ImGui::SameLine();
103 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
104 if (ImGui::SliderFloat("##marker_size_slider", &global_size_slider, minScale, maxScale, "x%.2f"))
105 {
106 // Clamp just in case
107 if (global_size_slider < minScale)
108 global_size_slider = minScale;
109 if (global_size_slider > maxScale)
110 global_size_slider = maxScale;
111 }
112 }
113
114 ImGui::Separator();
115
116 float minScale = 25.0f, maxScale = 550.0f;
117 ImGui::Separator();
118 ImGui::TextUnformatted("Directory Thumb Pixel Size");
119 ImGui::SameLine();
120 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
121 if (ImGui::SliderFloat("##image_size_slider", &imageSize, minScale, maxScale, "x%.2f"))
122 {
123 // Clamp just in case
124 if (imageSize < minScale)
125 imageSize = minScale;
126 if (imageSize > maxScale)
127 imageSize = maxScale;
128 }
129
130 ImGui::Separator();
131
132 ImGui::BeginChild("DirectoryScrollRegion", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar);
133 float window_width = ImGui::GetWindowWidth();
134 ImVec2 content_region = ImGui::GetContentRegionAvail();
135 int columns = (int)(content_region.x / imageSize);
136 if (columns <= 0)
137 columns = 1;
138 int count = 0;
139 {
140
141 std::shared_lock<std::shared_mutex> lock(imagesMutex);
142 for (auto& image : images)
143 {
144 if (count % columns != 0)
145 ImGui::SameLine();
146 std::string path_file = directoryPath + "\\" + image.filename.c_str();
147 if (image.textureID == 0)
148 {
149 image = LoadTextureFromFile(path_file.c_str());
150 }
151
152 ImGui::BeginGroup();
153 ImGui::PushID(count);
154
156 {
157 // Add drag-and-drop functionality for marker images
158 if (ImGui::ImageButton((void*)(intptr_t)image.textureID, ImVec2(imageSize, imageSize)))
159 {
160 selected_image = image;
161 std::cout << "Selected Image: " << image.filename << " | " << image.textureID << std::endl;
162 ImGui::OpenPopup("Image Popup");
163 }
164
165 // Begin the drag source for the markers
166 if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID))
167 {
168 ImGui::SetDragDropPayload("MARKER_IMAGE", &image, sizeof(image)); // Attach the marker image payload
169 ImGui::Text("Drag Marker: %s", image.filename.c_str()); // Tooltip while dragging
170 ImGui::EndDragDropSource();
171 }
172 }
173 else
174 {
175 if (ImGui::ImageButton((void*)(intptr_t)image.textureID, ImVec2(imageSize, imageSize)))
176 {
177 selected_image = image;
178 std::cout << "Selected Image: " << image.filename << " | " << image.textureID << std::endl;
179 ImGui::OpenPopup("Image Popup");
180 }
181
182 if (ImGui::BeginPopup("Image Popup"))
183 {
184 ImGui::Text("File: %s", image.filename.c_str());
185 ImGui::EndPopup();
186 }
187 }
188 ImGui::TextWrapped("%s", TruncateString(image.filename.c_str(), 16).c_str());
189 ImGui::PopID();
190 ImGui::EndGroup();
191 count++;
192 }
193 }
194 ImGui::EndChild();
195 ImGui::End();
196 }
197
198 // Método genérico para retornar a imagem selecionada
200 {
201 return selected_image;
202 }
203
204 // Método para limpar a imagem selecionada
206 {
207 selected_image = {0, glm::vec2(), ""}; // Limpa a seleção
208 }
209
211 {
213 }
214
216 {
217 running.exchange(false, std::memory_order_relaxed);
218 if (monitorThread.joinable())
219 {
220 monitorThread.join();
221 }
222 }
223
225 {
227 }
228
229 ImageData getImageByPath(const std::string& file_name)
230 {
231 auto it = std::find_if(images.begin(), images.end(), [&](const ImageData& image)
232 {
233 // Check if image.filename is a substring at the end of file_name
234 if (file_name.size() >= image.filename.size()) {
235 return file_name.compare(file_name.size() - image.filename.size(), image.filename.size(), image.filename) == 0;
236 }
237 return false; });
238
239 if (it != images.end())
240 {
241 return *it;
242 }
243 else
244 {
245 throw std::runtime_error("Image not found: " + file_name);
246 }
247 }
248
250 {
251 return global_size_slider;
252 }
253
254 std::string directoryPath;
255 std::string directoryName;
256
258 {
259 // 1) Steal queues quickly (minimize lock time)
260 std::vector<std::string> removals;
261 std::vector<std::pair<std::string, std::string>> adds;
262 {
263 std::lock_guard<std::mutex> qlk(pendingMtx);
264 removals.swap(pendingRemovals);
265 adds.swap(pendingAddPaths);
266 }
267
268 // 2) Process removals (GL-safe thread)
269 if (!removals.empty())
270 {
271 std::unique_lock<std::shared_mutex> lk(imagesMutex);
272 for (const auto& fname : removals)
273 {
274 auto it = std::find_if(images.begin(), images.end(),
275 [&](const ImageData& im)
276 { return im.filename == fname; });
277 if (it != images.end())
278 {
279 if (it->textureID != 0)
280 glDeleteTextures(1, &it->textureID);
281 images.erase(it);
282 }
283 // else: already gone — ignore
284 }
285 }
286
287 // 3) Process additions (load textures here; GL-safe thread)
288 if (!adds.empty())
289 {
290 std::vector<ImageData> toInsert;
291 toInsert.reserve(adds.size());
292
293 for (auto& [fname, fullpath] : adds)
294 {
295 // Optional: wait briefly if file is still being copied
296 bool ready = false;
297 for (int i = 0; i < 10; ++i)
298 {
299 std::ifstream f(fullpath, std::ios::binary);
300 if (f.good())
301 {
302 ready = true;
303 break;
304 }
305 std::this_thread::sleep_for(std::chrono::milliseconds(50));
306 }
307 if (!ready)
308 continue;
309
310 // Use your existing loader (returns ImageData with texture + size)
311 ImageData img = LoadTextureFromFile(fullpath.c_str());
312 if (img.textureID != 0)
313 {
314 img.filename = fname; // store filename (not full path) for matching
315 toInsert.emplace_back(std::move(img));
316 }
317 }
318
319 if (!toInsert.empty())
320 {
321 std::unique_lock<std::shared_mutex> lk(imagesMutex);
322 for (auto& im : toInsert)
323 {
324 // Avoid duplicates
325 auto it = std::find_if(images.begin(), images.end(),
326 [&](const ImageData& x)
327 { return x.filename == im.filename; });
328 if (it == images.end())
329 images.emplace_back(std::move(im));
330 }
331 }
332 }
333 }
334
335private:
336 ImageData selected_image = {0, glm::vec2(), ""}; // Armazena a imagem selecionada
337 std::vector<ImageData> images;
338 std::thread monitorThread;
339 std::atomic<bool> running{true};
340 std::atomic<bool> first_scan_done{true};
341 std::shared_mutex imagesMutex;
343 float global_size_slider = 1.0f;
344 float imageSize = 128.0f;
345 std::mutex pendingMtx;
346 std::vector<std::string> pendingRemovals; // filenames to remove
347 std::vector<std::pair<std::string, std::string>> pendingAddPaths;
348
349 // Function to find an ImageData by filename
350 std::vector<ImageData>::iterator findImageByFilename(std::vector<ImageData>& images, const std::string& filename)
351 {
352 return std::find_if(images.begin(), images.end(), [&filename](const ImageData& image)
353 { return image.filename == filename; });
354 }
355
356 void monitorDirectory(const std::string& path)
357 {
358 using namespace std::chrono_literals;
359
360 // Keep only a simple "known file list" of filenames (no GL/state here)
361 std::vector<std::string> knownFiles;
362
363 while (running.load(std::memory_order_relaxed))
364 {
365 std::vector<std::string> currentFiles;
366 try
367 {
368 for (const auto& entry : std::filesystem::directory_iterator(path))
369 {
370 if (entry.is_regular_file())
371 {
372 currentFiles.push_back(entry.path().filename().string());
373 }
374 }
375 }
376 catch (const std::filesystem::filesystem_error& e)
377 {
378 std::cerr << "[DirectoryWindow] Filesystem error: " << e.what() << std::endl;
379 }
380
381 // Diff: removed = known - current
382 for (const auto& name : knownFiles)
383 {
384 if (std::find(currentFiles.begin(), currentFiles.end(), name) == currentFiles.end())
385 {
386 std::lock_guard<std::mutex> qlk(pendingMtx);
387 pendingRemovals.emplace_back(name);
388 }
389 }
390
391 // Diff: added = current - known
392 for (const auto& name : currentFiles)
393 {
394 if (std::find(knownFiles.begin(), knownFiles.end(), name) == knownFiles.end())
395 {
396 const std::string full = (std::filesystem::path(path) / name).string();
397 std::lock_guard<std::mutex> qlk(pendingMtx);
398 pendingAddPaths.emplace_back(name, full);
399 }
400 }
401
402 knownFiles = std::move(currentFiles);
403
404 // First pass complete → allow generateTextureIDs() to proceed
405 first_scan_done = true;
406
407 std::this_thread::sleep_for(2s);
408 }
409 }
410
412 {
413
414 // Record the start time using std::chrono::high_resolution_clock
415 //std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
416
417 int width, height, nrChannels;
418 unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 4);
419 stbi_set_flip_vertically_on_load(0); // Flip images vertically if needed
420 if (!data)
421 {
422 std::cerr << "Failed to load texture: " << path << std::endl;
423 return ImageData(0, glm::vec2(), "");
424 }
425
426 GLuint textureID[1];
427 glGenTextures(1, textureID);
428 glBindTexture(GL_TEXTURE_2D, textureID[0]);
429
430 // Set the texture data
431 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
432
433 GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
434 GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
435 GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
436 GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
437
438 stbi_image_free(data);
439
440 // Record the end time using std::chrono::high_resolution_clock
441 //std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
442
443 // Calculate the duration in milliseconds
444 //std::chrono::seconds duration = std::chrono::duration_cast<std::chrono::seconds>(end - start);
445
446 // Output the duration in milliseconds
447 //std::cout << "Operation for file "<< path <<"took " << duration.count() << " seconds" << std::endl;
448
449 return ImageData(textureID[0], glm::vec2(width, height), path);
450 }
451};
DirectoryKind
#define GLCall(x)
Definition Renderer.h:12
DirectoryKind kind_
std::string TruncateString(const std::string &str, size_t maxLength)
std::shared_mutex imagesMutex
std::atomic< bool > running
std::mutex pendingMtx
ImageData getImageByPath(const std::string &file_name)
std::vector< ImageData > images
void applyPendingAssetChanges()
ImageData LoadTextureFromFile(const char *path)
std::thread monitorThread
std::vector< ImageData >::iterator findImageByFilename(std::vector< ImageData > &images, const std::string &filename)
std::atomic< bool > first_scan_done
ImageData getSelectedImage() const
std::string directoryPath
std::vector< std::string > pendingRemovals
float getGlobalSizeSlider()
std::string directoryName
ImageData selected_image
void monitorDirectory(const std::string &path)
DirectoryWindow(std::string directoryPath, std::string directoryName, DirectoryKind kind_)
std::vector< std::pair< std::string, std::string > > pendingAddPaths