Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 128 additions & 9 deletions src/WeatherEditor/EditorWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,25 @@ void EditorWindow::ShowObjectsWindow()
}
}

// Stable user IDs for sortable columns — used instead of ColumnIndex so reordering/insertion won't break sorting.
enum ColumnID : ImGuiID
{
ColFav = 0,
ColEditorID,
ColFormID,
ColFile,
ColStatus,
ColJson
};

// Create a table for the right column with "Name" and "ID" headers. Different weights to prevent truncation.
if (ImGui::BeginTable("DetailsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Sortable)) {
ImGui::TableSetupColumn("Fav", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoSort, 25.0f); // Favorite indicator
ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f); // Largest - weather/template names
ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 80.0f); // Fixed - 8 hex chars
ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Medium - plugin names
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f); // Smaller - status text
if (ImGui::BeginTable("DetailsTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Sortable)) {
ImGui::TableSetupColumn("Fav", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoSort, 38.0f, ColFav); // Favorite indicator
ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f, ColEditorID); // Largest - weather/template names
ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 90.0f, ColFormID); // Fixed - 8 hex chars
ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f, ColFile); // Medium - plugin names
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f, ColStatus); // Smaller - status text
ImGui::TableSetupColumn("json", ImGuiTableColumnFlags_WidthFixed, 55.0f, ColJson); // JSON file / delete

ImGui::TableHeadersRow();

Expand All @@ -288,7 +300,26 @@ void EditorWindow::ShowObjectsWindow()
if (sortSpecs->SpecsDirty) {
if (sortSpecs->SpecsCount > 0) {
const ImGuiTableColumnSortSpecs& spec = sortSpecs->Specs[0];
currentSortColumn = static_cast<SortColumn>(spec.ColumnIndex);
switch (spec.ColumnUserID) {
case ColEditorID:
currentSortColumn = SortColumn::EditorID;
break;
case ColFormID:
currentSortColumn = SortColumn::FormID;
break;
case ColFile:
currentSortColumn = SortColumn::File;
break;
case ColStatus:
currentSortColumn = SortColumn::Status;
break;
case ColJson:
currentSortColumn = SortColumn::JsonAttachment;
break;
default:
currentSortColumn = SortColumn::None;
break;
}
sortAscending = (spec.SortDirection == ImGuiSortDirection_Ascending);
} else {
currentSortColumn = SortColumn::None;
Expand All @@ -313,6 +344,7 @@ void EditorWindow::ShowObjectsWindow()
for (const auto& w : widgets) {
sortedWidgets.push_back(w.get());
}
RefreshJsonAttachmentCache(sortedWidgets);
if (currentSortColumn != SortColumn::None) {
Comment on lines +347 to 348

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

RefreshJsonAttachmentCache triggers O(N) filesystem I/O for all category widgets on first visit.

RefreshJsonAttachmentCache calls widget->HasSavedFile() (i.e., std::filesystem::exists()) for each widget not yet in the cache. Because this is called on sortedWidgets — the entire category list before filtering — all widgets in the category incur a filesystem hit on the first frame that category is viewed. For categories with many records (Skyrim has hundreds of weathers/lighting templates), this can cause a one-frame stall.

Consider populating the cache lazily in drawJsonDeleteButton (only for visible/filtered widgets) or batching it asynchronously. Alternatively, document the expected stall so it is a conscious trade-off.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WeatherEditor/EditorWindow.cpp` around lines 336 - 337,
RefreshJsonAttachmentCache currently iterates sortedWidgets and calls
widget->HasSavedFile() (std::filesystem::exists) for every widget on first view,
causing an O(N) filesystem hit; change the approach to populate the
JSON-attachment cache lazily by moving the exists() check into
drawJsonDeleteButton (or whatever function renders per-widget UI) so only
visible/filtered widgets trigger HasSavedFile(), or implement an async/batched
prefetch that marks cache entries without blocking the UI; update
RefreshJsonAttachmentCache to only initialize metadata for already-visible
widgets (use the same visibility/filtering logic used when building the display
list) and remove the global eager loop over sortedWidgets to avoid the one-frame
stall.

std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) {
int comparison = 0;
Expand All @@ -335,13 +367,41 @@ void EditorWindow::ShowObjectsWindow()
comparison = _stricmp(statusA.c_str(), statusB.c_str());
break;
}
case SortColumn::JsonAttachment:
{
bool aHasJson = HasCachedJsonAttachment(a);
bool bHasJson = HasCachedJsonAttachment(b);
comparison = static_cast<int>(aHasJson) - static_cast<int>(bHasJson);
break;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
default:
break;
}
return sortAscending ? (comparison < 0) : (comparison > 0);
});
}

// Helper lambda: renders the JSON delete button column for a widget
auto drawJsonDeleteButton = [&](Widget* widget) {
ImGui::TableNextColumn();
if (HasCachedJsonAttachment(widget)) {
auto* menu = globals::menu;
if (menu && menu->uiIcons.deleteSettings.texture) {
const float iconSize = ImGui::GetFrameHeight() * 0.85f;
auto _style = Util::ErrorButtonStyle();
ImGui::SetNextItemAllowOverlap();
char idBuf[32];
snprintf(idBuf, sizeof(idBuf), "##jsondel_%s", widget->GetFormID().c_str());
if (ImGui::ImageButton(idBuf, menu->uiIcons.deleteSettings.texture, { iconSize, iconSize })) {
pendingDeleteWidget = widget;
pendingDeletePopupRequested = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Delete JSON file");
}
}
};

// Special handling for Cell Lighting category
if (selectedCategory == "Cell Lighting") {
auto player = RE::PlayerCharacter::GetSingleton();
Expand Down Expand Up @@ -405,6 +465,9 @@ void EditorWindow::ShowObjectsWindow()
// Status column
ImGui::TableNextColumn();
ImGui::Text("Interior Cell");

// json column (empty for cells - no standalone json)
ImGui::TableNextColumn();
} else {
// Show message that cell lighting is only for interior cells
ImGui::TableNextRow();
Expand Down Expand Up @@ -467,7 +530,7 @@ void EditorWindow::ShowObjectsWindow()

// Editor ID column with [CURRENT] prefix
bool isSelected = sortedWidgets[i]->IsOpen();
if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_AllowOverlap)) {
if (ImGui::IsMouseDoubleClicked(0)) {
sortedWidgets[i]->SetOpen(true);
AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory);
Expand Down Expand Up @@ -512,6 +575,9 @@ void EditorWindow::ShowObjectsWindow()
if (markedRecord != settings.markedRecords.end()) {
ImGui::Text("%s", markedRecord->second.c_str());
}

// json / delete column
drawJsonDeleteButton(sortedWidgets[i]);
}
}

Expand Down Expand Up @@ -555,7 +621,7 @@ void EditorWindow::ShowObjectsWindow()

// Editor ID column
bool isSelected = sortedWidgets[i]->IsOpen();
if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_AllowOverlap)) {
if (ImGui::IsMouseDoubleClicked(0)) {
sortedWidgets[i]->SetOpen(true);
AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory);
Expand Down Expand Up @@ -633,6 +699,9 @@ void EditorWindow::ShowObjectsWindow()
if (markedRecord != settings.markedRecords.end()) {
ImGui::Text("%s", markedRecord->second.c_str());
}

// json / delete column
drawJsonDeleteButton(sortedWidgets[i]);
}

ImGui::EndTable(); // End DetailsTable
Expand All @@ -644,6 +713,18 @@ void EditorWindow::ShowObjectsWindow()
ImGui::EndTable(); // End ObjectTable
} // End if BeginTable("ObjectTable")

// Confirmation modal for json deletion - must be outside BeginChild so the modal can block the root window
if (pendingDeleteWidget) {
if (pendingDeletePopupRequested) {
ImGui::OpenPopup("ListDeleteConfirmation");
pendingDeletePopupRequested = false;
}
pendingDeleteWidget->DrawDeleteConfirmationModal("ListDeleteConfirmation");
if (!ImGui::IsPopupOpen("ListDeleteConfirmation")) {
pendingDeleteWidget = nullptr;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// End the window
ImGui::End();
}
Expand Down Expand Up @@ -1127,6 +1208,7 @@ void EditorWindow::SetupResources()
{
Load();
PaletteWindow::GetSingleton()->Load();
InvalidateJsonAttachmentCache();

// Populate all widget collections using WidgetFactory templates
WidgetFactory::PopulateWidgets<WeatherWidget, RE::TESWeather>(weatherWidgets);
Expand Down Expand Up @@ -1776,6 +1858,43 @@ void EditorWindow::RenderNotifications()
}
}

void EditorWindow::RefreshJsonAttachmentCache(const std::vector<Widget*>& widgets)
{
for (auto* widget : widgets) {
if (!widget) {
continue;
}
if (!jsonAttachmentCache.contains(widget)) {
jsonAttachmentCache.emplace(widget, widget->HasSavedFile());
}
}
}

bool EditorWindow::HasCachedJsonAttachment(Widget* widget) const
{
if (!widget) {
return false;
}
if (auto it = jsonAttachmentCache.find(widget); it != jsonAttachmentCache.end()) {
return it->second;
}
return false;
}

void EditorWindow::InvalidateJsonAttachmentCache(Widget* widget)
{
if (widget) {
jsonAttachmentCache.erase(widget);
return;
}
jsonAttachmentCache.clear();
}

void EditorWindow::OnWidgetJsonAttachmentChanged(Widget* widget)
{
InvalidateJsonAttachmentCache(widget);
}

void EditorWindow::AddToRecent(const std::string& widgetId, const std::string& category)
{
auto& categoryRecent = settings.recentWidgets[category];
Expand Down
16 changes: 15 additions & 1 deletion src/WeatherEditor/EditorWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include "WeatherUtils.h"
#include "Widget.h"

#include <unordered_map>

class EditorWindow
{
public:
Expand Down Expand Up @@ -181,6 +183,8 @@ class EditorWindow
~EditorWindow();

private:
friend class Widget;

void SaveAll();
void SaveSettings();
void LoadSettings();
Expand Down Expand Up @@ -208,8 +212,18 @@ class EditorWindow
EditorID,
FormID,
File,
Status
Status,
JsonAttachment
};
SortColumn currentSortColumn = SortColumn::None;
bool sortAscending = true;

Widget* pendingDeleteWidget = nullptr;
bool pendingDeletePopupRequested = false;

void OnWidgetJsonAttachmentChanged(Widget* widget);
std::unordered_map<Widget*, bool> jsonAttachmentCache;
void RefreshJsonAttachmentCache(const std::vector<Widget*>& widgets);
bool HasCachedJsonAttachment(Widget* widget) const;
void InvalidateJsonAttachmentCache(Widget* widget = nullptr);
};
18 changes: 10 additions & 8 deletions src/WeatherEditor/Widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ void Widget::Save()
}

settingsFile.close();
EditorWindow::GetSingleton()->OnWidgetJsonAttachmentChanged(this);

} catch (const nlohmann::json::exception& e) {
logger::error("{}: JSON error while saving settings: {}", GetEditorID(), e.what());
Expand Down Expand Up @@ -162,6 +163,8 @@ void Widget::Delete()
// Apply the vanilla values to the game
ApplyChanges();

EditorWindow::GetSingleton()->OnWidgetJsonAttachmentChanged(this);

EditorWindow::GetSingleton()->ShowNotification(
std::format("Deleted {} - reverted to vanilla values", GetEditorID()),
ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
Expand Down Expand Up @@ -202,14 +205,14 @@ void Widget::DrawMenu()
DrawDeleteConfirmationModal();
}

void Widget::DrawDeleteConfirmationModal()
void Widget::DrawDeleteConfirmationModal(const char* popupId)
{
if (!ImGui::IsPopupOpen("DeleteConfirmation"))
if (!ImGui::IsPopupOpen(popupId))
return;
if (deleteConfirmationFrame == ImGui::GetFrameCount())
return;

if (ImGui::BeginPopupModal("DeleteConfirmation", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
if (ImGui::BeginPopupModal(popupId, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
deleteConfirmationFrame = ImGui::GetFrameCount();
ImGui::Text("Are you sure you want to delete the saved settings file?");
ImGui::Spacing();
Expand Down Expand Up @@ -345,12 +348,11 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav

if (HasSavedFile() && menu->uiIcons.deleteSettings.texture) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f));
if (ImGui::ImageButton((std::string(searchId) + "_Delete").c_str(), menu->uiIcons.deleteSettings.texture, buttonSize)) {
ImGui::OpenPopup("DeleteConfirmation");
{
auto _style = Util::ErrorButtonStyle();
if (ImGui::ImageButton((std::string(searchId) + "_Delete").c_str(), menu->uiIcons.deleteSettings.texture, buttonSize))
ImGui::OpenPopup("DeleteConfirmation");
}
ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Delete saved file");
}
Expand Down
3 changes: 2 additions & 1 deletion src/WeatherEditor/Widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,14 @@ class Widget

bool MatchesSearch(const std::string& text) const;

void DrawDeleteConfirmationModal(const char* popupId = "DeleteConfirmation");

json js = json();

protected:
std::string cachedEditorID;
virtual void DrawMenu();
std::string GetFolderName();
void DrawDeleteConfirmationModal();
};

// Simple widget for caching form data without full widget functionality
Expand Down