From 2f5af51eee1128ab0bfa26213d9f319a6430e4c1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 6 Apr 2026 20:46:44 +0200 Subject: [PATCH] Add notes tab search --- src/Classes/EditControl.lua | 195 +++++++++++++++++++++++++++++++++++- src/Classes/NotesTab.lua | 91 ++++++++++++++++- 2 files changed, 283 insertions(+), 3 deletions(-) diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index 2964057adf..d2f008e31e 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -36,6 +36,46 @@ local function newlineCount(str) end end +local function getColorCodeLength(str, index) + if str:sub(index, index) ~= "^" then + return 0 + end + local nextChar = str:sub(index + 1, index + 1) + if nextChar == "x" and str:sub(index + 2, index + 7):match("^%x%x%x%x%x%x$") then + return 8 + elseif nextChar:match("^%d$") then + return 2 + end + return 0 +end + +local function buildVisibleLineMap(rawLine) + local visible = "" + local rawStarts = {} + local rawEnds = {} + local rawIndex = 1 + while rawIndex <= #rawLine do + local colorCodeLength = getColorCodeLength(rawLine, rawIndex) + if colorCodeLength > 0 then + rawIndex = rawIndex + colorCodeLength + else + local rawEnd = utf8.next(rawLine, rawIndex, 1) + if not rawEnd or rawEnd <= rawIndex then + rawEnd = rawIndex + 1 + end + local char = rawLine:sub(rawIndex, rawEnd - 1) + local visibleStart = #visible + 1 + visible = visible .. char + for offset = 0, #char - 1 do + rawStarts[visibleStart + offset] = rawIndex + rawEnds[visibleStart + offset] = rawEnd + end + rawIndex = rawEnd + end + end + return visible, rawStarts, rawEnds +end + local EditClass = newClass("EditControl", "ControlHost", "Control", "UndoHandler", "TooltipHost", function(self, anchor, rect, init, prompt, filter, limit, changeFunc, lineHeight, allowZoom, clearable) self.ControlHost() self.Control(anchor, rect) @@ -55,6 +95,14 @@ local EditClass = newClass("EditControl", "ControlHost", "Control", "UndoHandler self.disableCol = "^9" self.selCol = "^0" self.selBGCol = "^xBBBBBB" + self.searchBGFillCol = { 0.03, 0.03, 0.04, 0.78 } + self.searchFocusFillCol = { 0.03, 0.03, 0.04, 0.88 } + self.searchBGCol = { 0.58, 0.60, 0.64, 0.98 } + self.searchFocusBGCol = { 0.96, 0.97, 0.99, 1.00 } + self.searchQuery = "" + self.searchMatches = {} + self.searchMatchesByLine = {} + self.searchFocusIndex = nil self.blinkStart = GetTime() self.allowZoom = allowZoom local function buttonSize() @@ -238,6 +286,137 @@ function EditClass:MoveCaretVertically(offset) self.blinkStart = GetTime() end +function EditClass:SetSearchQuery(query, centerFocused) + query = tostring(query or "") + local resetFocus = query ~= self.searchQuery + self.searchQuery = query + self:RefreshSearch(centerFocused, resetFocus) +end + +function EditClass:AdvanceSearchMatch(direction) + local matchCount = #self.searchMatches + if matchCount == 0 then + return false + end + if direction and direction < 0 then + if not self.searchFocusIndex or self.searchFocusIndex <= 1 then + self.searchFocusIndex = matchCount + else + self.searchFocusIndex = self.searchFocusIndex - 1 + end + else + if not self.searchFocusIndex or self.searchFocusIndex >= matchCount then + self.searchFocusIndex = 1 + else + self.searchFocusIndex = self.searchFocusIndex + 1 + end + end + self:CenterOnSearchMatch(self.searchFocusIndex) + return true +end + +function EditClass:RefreshSearch(centerFocused, resetFocus) + local query = self.searchQuery or "" + local lowerQuery = query:lower() + local previousFocus = self.searchFocusIndex + self.searchMatches = {} + self.searchMatchesByLine = {} + self.searchFocusIndex = nil + if query == "" then + return + end + + local lineIndex = 0 + for s, line in (self.buf.."\n"):gmatch("()([^\n]*)\n") do + lineIndex = lineIndex + 1 + local visibleLine, rawStarts, rawEnds = buildVisibleLineMap(line) + local searchLine = visibleLine:lower() + local searchStart = 1 + while true do + local visibleStart, visibleEnd = searchLine:find(lowerQuery, searchStart, true) + if not visibleStart then + break + end + local rawStart = rawStarts[visibleStart] + local rawEnd = rawEnds[visibleEnd] + if rawStart and rawEnd then + local matchIndex = #self.searchMatches + 1 + local match = { + index = matchIndex, + lineIndex = lineIndex, + line = line, + rawStart = rawStart, + rawEnd = rawEnd, + } + self.searchMatches[matchIndex] = match + self.searchMatchesByLine[lineIndex] = self.searchMatchesByLine[lineIndex] or {} + table.insert(self.searchMatchesByLine[lineIndex], match) + end + searchStart = visibleStart + 1 + end + end + + if #self.searchMatches > 0 then + if not resetFocus and previousFocus then + self.searchFocusIndex = m_min(previousFocus, #self.searchMatches) + else + self.searchFocusIndex = 1 + end + if centerFocused then + self:CenterOnSearchMatch(self.searchFocusIndex) + end + end +end + +function EditClass:CenterOnSearchMatch(matchIndex) + if not self.lineHeight then + return + end + local match = self.searchMatches[matchIndex] + if not match then + return + end + self:UpdateScrollBars() + if self.controls.scrollBarV.enabled then + local targetY = (match.lineIndex - 1) * self.lineHeight + self.controls.scrollBarV:SetOffset(targetY - (self.controls.scrollBarV.viewDim - self.lineHeight) / 2) + end + if self.controls.scrollBarH.enabled then + local matchStartX = DrawStringWidth(self.lineHeight, self.font, match.line:sub(1, match.rawStart - 1)) + local matchWidth = DrawStringWidth(self.lineHeight, self.font, match.line:sub(match.rawStart, match.rawEnd - 1)) + self.controls.scrollBarH:SetOffset(matchStartX + matchWidth / 2 - self.controls.scrollBarH.viewDim / 2) + end +end + +function EditClass:DrawSearchHighlightsForLine(lineIndex, line, textX, textY, textHeight) + local matches = self.searchMatchesByLine[lineIndex] + if not matches then + return + end + for _, match in ipairs(matches) do + local matchStartX = DrawStringWidth(textHeight, self.font, line:sub(1, match.rawStart - 1)) + local matchWidth = DrawStringWidth(textHeight, self.font, line:sub(match.rawStart, match.rawEnd - 1)) + if matchWidth > 0 then + local isFocused = match.index == self.searchFocusIndex + local fillColor = isFocused and self.searchFocusFillCol or self.searchBGFillCol + local borderColor = isFocused and self.searchFocusBGCol or self.searchBGCol + local drawX = textX + matchStartX - 2 + local drawWidth = matchWidth + 4 + local borderX = drawX - 1 + local borderY = textY - 1 + local borderWidth = drawWidth + 2 + local borderHeight = textHeight + 2 + SetDrawColor(fillColor[1], fillColor[2], fillColor[3], fillColor[4]) + DrawImage(nil, drawX, textY, drawWidth, textHeight) + SetDrawColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4]) + DrawImage(nil, borderX, borderY, borderWidth, 2) + DrawImage(nil, borderX, borderY + borderHeight - 2, borderWidth, 2) + DrawImage(nil, borderX, borderY, 2, borderHeight) + DrawImage(nil, borderX + borderWidth - 2, borderY, 2, borderHeight) + end + end +end + function EditClass:Draw(viewPort, noTooltip) local x, y = self:GetPos() local width, height = self:GetSize() @@ -299,6 +478,16 @@ function EditClass:Draw(viewPort, noTooltip) if self.inactiveText then local inactiveText = type(inactiveText) == "string" and self.inactiveText or self.inactiveText(self.buf) DrawString(-self.controls.scrollBarH.offset, -self.controls.scrollBarV.offset, "LEFT", textHeight, self.font, inactiveText) + elseif self.lineHeight and #self.searchMatches > 0 then + local lineIndex = 0 + local drawY = -self.controls.scrollBarV.offset + for line in (self.buf.."\n"):gmatch("([^\n]*)\n") do + lineIndex = lineIndex + 1 + self:DrawSearchHighlightsForLine(lineIndex, line, -self.controls.scrollBarH.offset, drawY, textHeight) + SetDrawColor(self.inactiveCol) + DrawString(-self.controls.scrollBarH.offset, drawY, "LEFT", textHeight, self.font, line) + drawY = drawY + textHeight + end elseif self.protected then DrawString(-self.controls.scrollBarH.offset, -self.controls.scrollBarV.offset, "LEFT", textHeight, self.font, string.rep(protected_replace, #self.buf)) else @@ -324,9 +513,13 @@ function EditClass:Draw(viewPort, noTooltip) local left = m_min(self.caret, self.sel or self.caret) local right = m_max(self.caret, self.sel or self.caret) local caretX + local lineIndex = 0 SetDrawColor(self.textCol) for s, line, e in (self.buf.."\n"):gmatch("()([^\n]*)\n()") do + lineIndex = lineIndex + 1 textX = -self.controls.scrollBarH.offset + self:DrawSearchHighlightsForLine(lineIndex, line, textX, textY, textHeight) + SetDrawColor(self.textCol) if left >= e or right <= s then DrawString(textX, textY, "LEFT", textHeight, self.font, line) end @@ -507,7 +700,7 @@ function EditClass:OnKeyDown(key, doubleClick) if self.enterFunc then self.enterFunc(self.buf) end - return + return self end elseif key == "a" and ctrl then self:SelectAll() diff --git a/src/Classes/NotesTab.lua b/src/Classes/NotesTab.lua index f78ea2eb41..0d30c1ad10 100644 --- a/src/Classes/NotesTab.lua +++ b/src/Classes/NotesTab.lua @@ -4,6 +4,7 @@ -- Notes tab for the current build. -- local t_insert = table.insert +local m_floor = math.floor local NotesTabClass = newClass("NotesTab", "ControlHost", "Control", function(self, build) self.ControlHost() @@ -31,14 +32,75 @@ 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.fire,"TOPLEFT"}, {0, 48, 0, 0}, "", nil, "^%C\t\n", nil, function() + self.controls.edit:RefreshSearch() + end, 16, true) self.controls.edit.width = function() return self.width - 16 end self.controls.edit.height = function() return self.height - 128 end - self.controls.toggleColorCodes = new("ButtonControl", {"TOPRIGHT",self,"TOPRIGHT"}, {-10, 70, 160, 20}, "Show Color Codes", function() + + self.controls.searchClear = new("ButtonControl", {"TOPRIGHT",self,"TOPRIGHT"}, {-10, 10, 20, 20}, "x", function() + self:ClearSearch() + end) + self.controls.searchClear.tooltipText = function() + return "Clear search" + end + self.controls.searchClear.enabled = function() + return self.controls.search.buf ~= "" + end + self.controls.searchNext = new("ButtonControl", {"RIGHT",self.controls.searchClear,"LEFT"}, {-4, 0, 24, 20}, "\\/", function() + self:AdvanceSearch(1) + end) + self.controls.searchNext.tooltipText = function() + return "Next match\n\nShortcut: Enter" + end + self.controls.searchNext.enabled = function() + return #self.controls.edit.searchMatches > 0 + end + self.controls.searchPrev = new("ButtonControl", {"RIGHT",self.controls.searchNext,"LEFT"}, {-4, 0, 24, 20}, "/\\", function() + self:AdvanceSearch(-1) + end) + self.controls.searchPrev.tooltipText = function() + return "Previous match\n\nShortcut: Shift+Enter" + end + self.controls.searchPrev.enabled = function() + return #self.controls.edit.searchMatches > 0 + end + self.controls.search = new("EditControl", {"RIGHT",self.controls.searchPrev,"LEFT"}, {-8, 0, 220, 20}, "", nil, "%c", 100, function(buf) + self.controls.edit:SetSearchQuery(buf, true) + end) + self.controls.search.width = function() + local baseWidth = math.max(140, math.min(320, self.width - 700)) + return math.max(100, m_floor(baseWidth * 0.7)) + end + self.controls.search.enterFunc = function() + self:AdvanceSearch(IsKeyDown("SHIFT") and -1 or 1) + end + self.controls.search:SetPlaceholder("Search") + + self.controls.searchCount = new("LabelControl", {"RIGHT",self.controls.search,"LEFT"}, {-8, 0, 60, 16}, function() + if self.controls.search.buf == "" then + return "" + end + local matchCount = #self.controls.edit.searchMatches + if matchCount == 0 then + return "^10/0" + end + return ("^7%d/%d"):format(self.controls.edit.searchFocusIndex or 0, matchCount) + end) + self.controls.searchCount.x = function() + local reservedWidth = self.controls.searchCount:GetProperty("width") + local labelWidth = DrawStringWidth(self.controls.searchCount:GetProperty("height"), "VAR", self.controls.searchCount:GetProperty("label")) + return reservedWidth - 8 - labelWidth + end + self.controls.searchCount.width = function() + return DrawStringWidth(self.controls.searchCount:GetProperty("height"), "VAR", "^7999/9999") + 8 + end + + self.controls.toggleColorCodes = new("ButtonControl", {"TOPRIGHT",self,"TOPRIGHT"}, {-10, 38, 160, 20}, "Show Color Codes", function() self.showColorCodes = not self.showColorCodes self:SetShowColorCodes(self.showColorCodes) end) @@ -54,6 +116,7 @@ function NotesTabClass:SetShowColorCodes(setting) self.controls.toggleColorCodes.label = "Show Color Codes" self.controls.edit.buf = self.controls.edit.buf:gsub("%^_x(%x%x%x%x%x%x)","^x%1"):gsub("%^_(%d)","^%1") end + self.controls.edit:RefreshSearch(#self.controls.edit.searchMatches > 0) end function NotesTabClass:SetColor(color) @@ -82,6 +145,19 @@ function NotesTabClass:Save(xml) self.lastContent = self.controls.edit.buf end +function NotesTabClass:ClearSearch() + self.controls.search:SetText("", true) + self.controls.search:SelectAll() + self:SelectControl(self.controls.search) + return self.controls.search +end + +function NotesTabClass:AdvanceSearch(direction) + self.controls.edit:AdvanceSearchMatch(direction) + self:SelectControl(self.controls.search) + return self.controls.search +end + function NotesTabClass:Draw(viewPort, inputEvents) self.x = viewPort.x self.y = viewPort.y @@ -94,6 +170,17 @@ function NotesTabClass:Draw(viewPort, inputEvents) self.controls.edit:Undo() elseif event.key == "y" and IsKeyDown("CTRL") then self.controls.edit:Redo() + elseif event.key == "f" and IsKeyDown("CTRL") then + self:SelectControl(self.controls.search) + self.controls.search:SelectAll() + inputEvents[id] = nil + elseif event.key == "ESCAPE" and self.controls.search.hasFocus then + if self.controls.search.buf ~= "" then + self:ClearSearch() + else + self:SelectControl(self.controls.edit) + end + inputEvents[id] = nil end end end