Skip to content
Open
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
101 changes: 101 additions & 0 deletions src/Classes/NotesListControl.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
-- Path of Building
--
-- Class: Note List
-- Note list control.
--
local t_insert = table.insert
local t_remove = table.remove
local m_max = math.max

local NotesListClass = newClass("NotesListControl", "ListControl", function(self, anchor, rect, notesTab)
self.ListControl(anchor, rect, 16, "VERTICAL", true, notesTab.notesOrderList)
self.notesTab = notesTab

self.label = "^7Notes:"
self.controls.delete = new("ButtonControl", {"BOTTOMRIGHT",self,"TOPRIGHT"}, {0, -2, 60, 18}, "Delete", function()
self:OnSelDelete(self.selIndex, self.selValue)
end)

self.controls.delete.enabled = function()
return self.selValue ~= nil and #self.list > 1
end

self.controls.rename = new("ButtonControl", {"RIGHT",self.controls.delete,"LEFT"}, {-2, 0, 60, 18}, "Rename", function()
self:RenameNote(notesTab.notes[self.selValue])
end)
self.controls.rename.enabled = function()
return self.selValue ~= nil
end

self.controls.new = new("ButtonControl", {"RIGHT",self.controls.rename,"LEFT"}, {-2, 0, 60, 18}, "New", function()
self:RenameNote(notesTab:NewNote(), true)
end)
end)

-- Triggered when the order of the list has changed
function NotesListClass:OnOrderChange(selIndex, selDragIndex)
self.notesTab.modFlag = true
end

function NotesListClass:OnSelDelete(index, noteId)
local note = self.notesTab.notes[noteId]
if #self.list > 1 then
main:OpenConfirmPopup("Delete Note", "Are you sure you want to delete '"..(note.title or "Default").."'?", "Delete", function()
t_remove(self.list, index)
self.notesTab.notes[noteId] = nil

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.

Deleting a note is not retained through the normal save flow. This callback removes the note but never marks notesTab.modFlag; with no other changes, the subsequent Notes draw keeps it false, Save remains disabled, and leaving the build skips the save prompt. Reopening restores the deleted note.

AI-assisted review disclosure: This finding was identified during a review using OpenAI Codex and confirmed with a focused reproduction.


self.selIndex = nil
self.selValue = nil

if noteId == self.notesTab.activeNoteId then
self.notesTab:SetActiveNote(self.list[m_max(1, index - 1)])
end
end)
end
end

-- Get the value to display in the list row
function NotesListClass:GetRowValue(column, index, noteId)
local note = self.notesTab.notes[noteId]
if column == 1 then
return (note.title or "Default") .. (noteId == self.notesTab.activeNoteId and " ^9(Current)" or "")
end
end

function NotesListClass:RenameNote(note, addOnName)
local controls = { }
controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this note:")
controls.edit = new("EditControl", nil, {0, 40, 350, 20}, note.title, nil, nil, 100, function(buf)
controls.save.enabled = buf:match("%S")
end)
controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function()
note.title = controls.edit.buf
self.notesTab.modFlag = true

if addOnName then
t_insert(self.list, note.id)
self.selIndex = #self.list
self.selValue = note.id
end

self.notesTab:SetActiveNote(note.id)
main:ClosePopup()
end)
controls.save.enabled = false
controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function()
if addOnName then
self.notesTab.notes[note.id] = nil
end
main:ClosePopup()
end)
main:OpenPopup(370, 100, note.title and "Rename" or "Note Name", controls, "save", "edit", "cancel")
end

-- Triggered when a note is selected from the list
function NotesListClass:OnSelClick(index, noteId, doubleClick)
self.notesTab:SaveContentToNote(self.notesTab.activeNoteId)

if doubleClick and noteId ~= self.notesTab.activeNoteId then
self.notesTab:SetActiveNote(noteId)
end
end
123 changes: 112 additions & 11 deletions src/Classes/NotesTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@ local NotesTabClass = newClass("NotesTab", "ControlHost", "Control", function(se

self.build = build

self.lastContent = ""
self.notes = { }
self.notesOrderList = { }

self.showColorCodes = false

local listSize = 250

local notesDesc = [[^7You can use Ctrl +/- (or Ctrl+Scroll) to zoom in and out and Ctrl+0 to reset.
This field also supports different colors. Using the caret symbol (^) followed by a Hex code or a number (0-9) will set the color.
Below are some common color codes PoB uses: ]]
self.controls.notesDesc = new("LabelControl", {"TOPLEFT",self,"TOPLEFT"}, {8, 8, 150, 16}, notesDesc)

-- Notes group list
self.controls.noteList = new("NotesListControl", { "TOPLEFT", self, "TOPLEFT" }, { 8, 30, listSize - 16, 80 }, self)

self.controls.notesDesc = new("LabelControl", {"TOPLEFT",self.controls.noteList,"TOPRIGHT"}, {8, -22, 150, 16}, notesDesc)
self.controls.normal = new("ButtonControl", {"TOPLEFT",self.controls.notesDesc,"TOPLEFT"}, {0, 48, 100, 18}, colorCodes.NORMAL.."NORMAL", function() self:SetColor(colorCodes.NORMAL) end)
self.controls.magic = new("ButtonControl", {"TOPLEFT",self.controls.normal,"TOPLEFT"}, {120, 0, 100, 18}, colorCodes.MAGIC.."MAGIC", function() self:SetColor(colorCodes.MAGIC) end)
self.controls.rare = new("ButtonControl", {"TOPLEFT",self.controls.magic,"TOPLEFT"}, {120, 0, 100, 18}, colorCodes.RARE.."RARE", function() self:SetColor(colorCodes.RARE) end)
Expand All @@ -31,7 +39,7 @@ Below are some common color codes PoB uses: ]]
self.controls.intelligence = new("ButtonControl", {"TOPLEFT",self.controls.dexterity,"TOPLEFT"}, {120, 0, 100, 18}, colorCodes.INTELLIGENCE.."INTELLIGENCE", function() self:SetColor(colorCodes.INTELLIGENCE) end)
self.controls.default = new("ButtonControl", {"TOPLEFT",self.controls.intelligence,"TOPLEFT"}, {120, 0, 100, 18}, "^7DEFAULT", function() self:SetColor("^7") end)

self.controls.edit = new("EditControl", {"TOPLEFT",self.controls.fire,"TOPLEFT"}, {0, 48, 0, 0}, "", nil, "^%C\t\n", nil, nil, 16, true)
self.controls.edit = new("EditControl", {"TOPLEFT",self.controls.noteList,"BOTTOMLEFT"}, {0, 8, 0, 0}, "", nil, "^%C\t\n", nil, nil, 16, true)
self.controls.edit.width = function()
return self.width - 16
end
Expand All @@ -42,6 +50,9 @@ Below are some common color codes PoB uses: ]]
self.showColorCodes = not self.showColorCodes
self:SetShowColorCodes(self.showColorCodes)
end)

self:SetActiveNote(1)

self:SelectControl(self.controls.edit)
end)

Expand All @@ -68,18 +79,62 @@ function NotesTabClass:SetColor(color)
end

function NotesTabClass:Load(xml, fileName)
for _, node in ipairs(xml) do
self.activeNoteId = 0

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.

Existing builds with an empty <Notes> section cannot be saved. The XML parser produces an empty Notes node; this reset leaves activeNoteId at 0 with no notes[0], and opening Notes or saving the build raises a Lua error. The historical base fixture OccVortex.xml exercises this valid shape, and a parser/load/save comparison passed on the base and failed on this head.

AI-assisted review disclosure: This finding was identified during a review using OpenAI Codex and confirmed with a focused reproduction.

self.notes = { }
self.noteOrderList = { }

for index, node in ipairs(xml) do
-- backwards compatibility
if type(node) == "string" then
self.controls.edit:SetText(node)
self.notesOrderList[1] = 1
self.notes[1] = {
id = 1,
content = node
}

self:SetActiveNote(1)
else
local savedNoteId = tonumber(node.attrib.id)
self.notesOrderList[index] = savedNoteId

self.notes[savedNoteId] = {
id = savedNoteId,
title = node.attrib.title,
content = node[1] or ""
}

if node.attrib.active ~= nil then
self:SetActiveNote(savedNoteId)
end
end
end
self.lastContent = self.controls.edit.buf

self.modFlag = false
end

function NotesTabClass:Save(xml)
self:SetShowColorCodes(false)
t_insert(xml, self.controls.edit.buf)
self.lastContent = self.controls.edit.buf

self.notes[self.activeNoteId].content = self.controls.edit.buf

for _, noteId in ipairs(self.notesOrderList) do
local attrib = {
id = tostring(noteId),
title = self.notes[noteId].title,
}

if self.activeNoteId == noteId then
attrib.active = "1"
end

local note = {
elem = "Note",
attrib = attrib,
[1] = self.notes[noteId].content
}

t_insert(xml, note)
end
end

function NotesTabClass:Draw(viewPort, inputEvents)
Expand All @@ -91,17 +146,63 @@ function NotesTabClass:Draw(viewPort, inputEvents)
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "z" and IsKeyDown("CTRL") then
self.controls.edit:Undo()
if self.controls.edit.hasFocus then
self.controls.edit:Undo()
end
elseif event.key == "y" and IsKeyDown("CTRL") then
self.controls.edit:Redo()
if self.controls.edit.hasFocus then
self.controls.edit:Redo()
end
end
end
end

self:ProcessControlsInput(inputEvents, viewPort)

main:DrawBackground(viewPort)

self:DrawControls(viewPort)

self.modFlag = (self.lastContent ~= self.controls.edit.buf)
self.modFlag = (self.notes[self.activeNoteId].content ~= self.controls.edit.buf) or self.modFlag
end

-- Creates a new note
function NotesTabClass:NewNote(noteId)
local note = { id = noteId, content = "" }

if not noteId then
note.id = 1
while self.notes[note.id] do
note.id = note.id + 1
end
end

self.notes[note.id] = note

return note
end

-- Changes the active note
function NotesTabClass:SetActiveNote(noteId)
-- Initialize note if needed
if not self.notesOrderList[1] then
self.notesOrderList[1] = 1
self:NewNote(1)
end

if not noteId then
noteId = self.activeNoteId
end

if not self.notes[noteId] then
noteId = self.notesOrderList[1]
end

self.activeNoteId = noteId
self.notes[self.activeNoteId].lastContent = self.controls.edit.buf
self.controls.edit:SetText(self.notes[noteId].content)

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.

Editing a note and then creating a new note or selecting a loadout silently loses the edit. This call replaces the editor buffer before the previous note's content is copied back; the next save only updates the destination note, and reopening restores the previous text. A focused base/head load → switch → save → reload reproduction confirmed the loss.

AI-assisted review disclosure: This finding was identified during a review using OpenAI Codex and confirmed with a focused reproduction.

end

function NotesTabClass:SaveContentToNote(noteId)
self.notes[noteId].content = self.controls.edit.buf
end
11 changes: 10 additions & 1 deletion src/Modules/Build.lua
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin
t_insert(self.configTab.configSetOrderList, configSet.id)
configSet.title = loadout

local note = self.notesTab:NewNote(#self.notesTab.notes + 1)
t_insert(self.notesTab.notesOrderList, note.id)
note.title = loadout

self:SyncLoadouts()
self.modFlag = true
main:ClosePopup()
Expand Down Expand Up @@ -387,14 +391,16 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin
local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1
local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1
local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1
local oneNote = self.notesTab and #self.notesTab.notesOrderList == 1

local newSpecId = findNamedSetId(self.treeTab:GetSpecList(), value, self.treeListSpecialLinks)
local newItemId = oneItem and 1 or findSetId(self.itemsTab.itemSetOrderList, value, self.itemsTab.itemSets, self.itemListSpecialLinks)
local newSkillId = oneSkill and 1 or findSetId(self.skillsTab.skillSetOrderList, value, self.skillsTab.skillSets, self.skillListSpecialLinks)
local newConfigId = oneConfig and 1 or findSetId(self.configTab.configSetOrderList, value, self.configTab.configSets, self.configListSpecialLinks)
local newNoteId = oneNote and 1 or findSetId(self.notesTab.notesOrderList, value, self.notesTab.notes, self.noteListSpecialLinks)

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.

With multiple notes, loadout selection fails in two existing cases. A plain loadout without an exact note title produces nil, and the guard below returns before the tree, items, skills, config, or visible selector changes. A brace-linked loadout such as Leveling {A} instead reaches findSetId, but noteListSpecialLinks is never initialized, so the resolver raises a Lua error before any update. Both paths were reproduced with the exact resolver captured from this head.

AI-assisted review disclosure: This finding was identified during a review using OpenAI Codex and confirmed with a focused reproduction.


-- if exact match nor special grouping cannot find setIds, bail
if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then
if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil or newNoteId == nil then
return
end

Expand All @@ -410,6 +416,9 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin
if newConfigId ~= self.configTab.activeConfigSetId then
self.configTab:SetActiveConfigSet(newConfigId)
end
if newNoteId ~= self.notesTab.activeNoteId then
self.notesTab:SetActiveNote(newNoteId)
end

self.controls.buildLoadouts:SelByValue(value)
end)
Expand Down
Loading