Skip to content

Commit 2f5af51

Browse files
committed
Add notes tab search
1 parent d126ab7 commit 2f5af51

2 files changed

Lines changed: 283 additions & 3 deletions

File tree

src/Classes/EditControl.lua

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,46 @@ local function newlineCount(str)
3636
end
3737
end
3838

39+
local function getColorCodeLength(str, index)
40+
if str:sub(index, index) ~= "^" then
41+
return 0
42+
end
43+
local nextChar = str:sub(index + 1, index + 1)
44+
if nextChar == "x" and str:sub(index + 2, index + 7):match("^%x%x%x%x%x%x$") then
45+
return 8
46+
elseif nextChar:match("^%d$") then
47+
return 2
48+
end
49+
return 0
50+
end
51+
52+
local function buildVisibleLineMap(rawLine)
53+
local visible = ""
54+
local rawStarts = {}
55+
local rawEnds = {}
56+
local rawIndex = 1
57+
while rawIndex <= #rawLine do
58+
local colorCodeLength = getColorCodeLength(rawLine, rawIndex)
59+
if colorCodeLength > 0 then
60+
rawIndex = rawIndex + colorCodeLength
61+
else
62+
local rawEnd = utf8.next(rawLine, rawIndex, 1)
63+
if not rawEnd or rawEnd <= rawIndex then
64+
rawEnd = rawIndex + 1
65+
end
66+
local char = rawLine:sub(rawIndex, rawEnd - 1)
67+
local visibleStart = #visible + 1
68+
visible = visible .. char
69+
for offset = 0, #char - 1 do
70+
rawStarts[visibleStart + offset] = rawIndex
71+
rawEnds[visibleStart + offset] = rawEnd
72+
end
73+
rawIndex = rawEnd
74+
end
75+
end
76+
return visible, rawStarts, rawEnds
77+
end
78+
3979
local EditClass = newClass("EditControl", "ControlHost", "Control", "UndoHandler", "TooltipHost", function(self, anchor, rect, init, prompt, filter, limit, changeFunc, lineHeight, allowZoom, clearable)
4080
self.ControlHost()
4181
self.Control(anchor, rect)
@@ -55,6 +95,14 @@ local EditClass = newClass("EditControl", "ControlHost", "Control", "UndoHandler
5595
self.disableCol = "^9"
5696
self.selCol = "^0"
5797
self.selBGCol = "^xBBBBBB"
98+
self.searchBGFillCol = { 0.03, 0.03, 0.04, 0.78 }
99+
self.searchFocusFillCol = { 0.03, 0.03, 0.04, 0.88 }
100+
self.searchBGCol = { 0.58, 0.60, 0.64, 0.98 }
101+
self.searchFocusBGCol = { 0.96, 0.97, 0.99, 1.00 }
102+
self.searchQuery = ""
103+
self.searchMatches = {}
104+
self.searchMatchesByLine = {}
105+
self.searchFocusIndex = nil
58106
self.blinkStart = GetTime()
59107
self.allowZoom = allowZoom
60108
local function buttonSize()
@@ -238,6 +286,137 @@ function EditClass:MoveCaretVertically(offset)
238286
self.blinkStart = GetTime()
239287
end
240288

289+
function EditClass:SetSearchQuery(query, centerFocused)
290+
query = tostring(query or "")
291+
local resetFocus = query ~= self.searchQuery
292+
self.searchQuery = query
293+
self:RefreshSearch(centerFocused, resetFocus)
294+
end
295+
296+
function EditClass:AdvanceSearchMatch(direction)
297+
local matchCount = #self.searchMatches
298+
if matchCount == 0 then
299+
return false
300+
end
301+
if direction and direction < 0 then
302+
if not self.searchFocusIndex or self.searchFocusIndex <= 1 then
303+
self.searchFocusIndex = matchCount
304+
else
305+
self.searchFocusIndex = self.searchFocusIndex - 1
306+
end
307+
else
308+
if not self.searchFocusIndex or self.searchFocusIndex >= matchCount then
309+
self.searchFocusIndex = 1
310+
else
311+
self.searchFocusIndex = self.searchFocusIndex + 1
312+
end
313+
end
314+
self:CenterOnSearchMatch(self.searchFocusIndex)
315+
return true
316+
end
317+
318+
function EditClass:RefreshSearch(centerFocused, resetFocus)
319+
local query = self.searchQuery or ""
320+
local lowerQuery = query:lower()
321+
local previousFocus = self.searchFocusIndex
322+
self.searchMatches = {}
323+
self.searchMatchesByLine = {}
324+
self.searchFocusIndex = nil
325+
if query == "" then
326+
return
327+
end
328+
329+
local lineIndex = 0
330+
for s, line in (self.buf.."\n"):gmatch("()([^\n]*)\n") do
331+
lineIndex = lineIndex + 1
332+
local visibleLine, rawStarts, rawEnds = buildVisibleLineMap(line)
333+
local searchLine = visibleLine:lower()
334+
local searchStart = 1
335+
while true do
336+
local visibleStart, visibleEnd = searchLine:find(lowerQuery, searchStart, true)
337+
if not visibleStart then
338+
break
339+
end
340+
local rawStart = rawStarts[visibleStart]
341+
local rawEnd = rawEnds[visibleEnd]
342+
if rawStart and rawEnd then
343+
local matchIndex = #self.searchMatches + 1
344+
local match = {
345+
index = matchIndex,
346+
lineIndex = lineIndex,
347+
line = line,
348+
rawStart = rawStart,
349+
rawEnd = rawEnd,
350+
}
351+
self.searchMatches[matchIndex] = match
352+
self.searchMatchesByLine[lineIndex] = self.searchMatchesByLine[lineIndex] or {}
353+
table.insert(self.searchMatchesByLine[lineIndex], match)
354+
end
355+
searchStart = visibleStart + 1
356+
end
357+
end
358+
359+
if #self.searchMatches > 0 then
360+
if not resetFocus and previousFocus then
361+
self.searchFocusIndex = m_min(previousFocus, #self.searchMatches)
362+
else
363+
self.searchFocusIndex = 1
364+
end
365+
if centerFocused then
366+
self:CenterOnSearchMatch(self.searchFocusIndex)
367+
end
368+
end
369+
end
370+
371+
function EditClass:CenterOnSearchMatch(matchIndex)
372+
if not self.lineHeight then
373+
return
374+
end
375+
local match = self.searchMatches[matchIndex]
376+
if not match then
377+
return
378+
end
379+
self:UpdateScrollBars()
380+
if self.controls.scrollBarV.enabled then
381+
local targetY = (match.lineIndex - 1) * self.lineHeight
382+
self.controls.scrollBarV:SetOffset(targetY - (self.controls.scrollBarV.viewDim - self.lineHeight) / 2)
383+
end
384+
if self.controls.scrollBarH.enabled then
385+
local matchStartX = DrawStringWidth(self.lineHeight, self.font, match.line:sub(1, match.rawStart - 1))
386+
local matchWidth = DrawStringWidth(self.lineHeight, self.font, match.line:sub(match.rawStart, match.rawEnd - 1))
387+
self.controls.scrollBarH:SetOffset(matchStartX + matchWidth / 2 - self.controls.scrollBarH.viewDim / 2)
388+
end
389+
end
390+
391+
function EditClass:DrawSearchHighlightsForLine(lineIndex, line, textX, textY, textHeight)
392+
local matches = self.searchMatchesByLine[lineIndex]
393+
if not matches then
394+
return
395+
end
396+
for _, match in ipairs(matches) do
397+
local matchStartX = DrawStringWidth(textHeight, self.font, line:sub(1, match.rawStart - 1))
398+
local matchWidth = DrawStringWidth(textHeight, self.font, line:sub(match.rawStart, match.rawEnd - 1))
399+
if matchWidth > 0 then
400+
local isFocused = match.index == self.searchFocusIndex
401+
local fillColor = isFocused and self.searchFocusFillCol or self.searchBGFillCol
402+
local borderColor = isFocused and self.searchFocusBGCol or self.searchBGCol
403+
local drawX = textX + matchStartX - 2
404+
local drawWidth = matchWidth + 4
405+
local borderX = drawX - 1
406+
local borderY = textY - 1
407+
local borderWidth = drawWidth + 2
408+
local borderHeight = textHeight + 2
409+
SetDrawColor(fillColor[1], fillColor[2], fillColor[3], fillColor[4])
410+
DrawImage(nil, drawX, textY, drawWidth, textHeight)
411+
SetDrawColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4])
412+
DrawImage(nil, borderX, borderY, borderWidth, 2)
413+
DrawImage(nil, borderX, borderY + borderHeight - 2, borderWidth, 2)
414+
DrawImage(nil, borderX, borderY, 2, borderHeight)
415+
DrawImage(nil, borderX + borderWidth - 2, borderY, 2, borderHeight)
416+
end
417+
end
418+
end
419+
241420
function EditClass:Draw(viewPort, noTooltip)
242421
local x, y = self:GetPos()
243422
local width, height = self:GetSize()
@@ -299,6 +478,16 @@ function EditClass:Draw(viewPort, noTooltip)
299478
if self.inactiveText then
300479
local inactiveText = type(inactiveText) == "string" and self.inactiveText or self.inactiveText(self.buf)
301480
DrawString(-self.controls.scrollBarH.offset, -self.controls.scrollBarV.offset, "LEFT", textHeight, self.font, inactiveText)
481+
elseif self.lineHeight and #self.searchMatches > 0 then
482+
local lineIndex = 0
483+
local drawY = -self.controls.scrollBarV.offset
484+
for line in (self.buf.."\n"):gmatch("([^\n]*)\n") do
485+
lineIndex = lineIndex + 1
486+
self:DrawSearchHighlightsForLine(lineIndex, line, -self.controls.scrollBarH.offset, drawY, textHeight)
487+
SetDrawColor(self.inactiveCol)
488+
DrawString(-self.controls.scrollBarH.offset, drawY, "LEFT", textHeight, self.font, line)
489+
drawY = drawY + textHeight
490+
end
302491
elseif self.protected then
303492
DrawString(-self.controls.scrollBarH.offset, -self.controls.scrollBarV.offset, "LEFT", textHeight, self.font, string.rep(protected_replace, #self.buf))
304493
else
@@ -324,9 +513,13 @@ function EditClass:Draw(viewPort, noTooltip)
324513
local left = m_min(self.caret, self.sel or self.caret)
325514
local right = m_max(self.caret, self.sel or self.caret)
326515
local caretX
516+
local lineIndex = 0
327517
SetDrawColor(self.textCol)
328518
for s, line, e in (self.buf.."\n"):gmatch("()([^\n]*)\n()") do
519+
lineIndex = lineIndex + 1
329520
textX = -self.controls.scrollBarH.offset
521+
self:DrawSearchHighlightsForLine(lineIndex, line, textX, textY, textHeight)
522+
SetDrawColor(self.textCol)
330523
if left >= e or right <= s then
331524
DrawString(textX, textY, "LEFT", textHeight, self.font, line)
332525
end
@@ -507,7 +700,7 @@ function EditClass:OnKeyDown(key, doubleClick)
507700
if self.enterFunc then
508701
self.enterFunc(self.buf)
509702
end
510-
return
703+
return self
511704
end
512705
elseif key == "a" and ctrl then
513706
self:SelectAll()

src/Classes/NotesTab.lua

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
-- Notes tab for the current build.
55
--
66
local t_insert = table.insert
7+
local m_floor = math.floor
78

89
local NotesTabClass = newClass("NotesTab", "ControlHost", "Control", function(self, build)
910
self.ControlHost()
@@ -31,14 +32,75 @@ Below are some common color codes PoB uses: ]]
3132
self.controls.intelligence = new("ButtonControl", {"TOPLEFT",self.controls.dexterity,"TOPLEFT"}, {120, 0, 100, 18}, colorCodes.INTELLIGENCE.."INTELLIGENCE", function() self:SetColor(colorCodes.INTELLIGENCE) end)
3233
self.controls.default = new("ButtonControl", {"TOPLEFT",self.controls.intelligence,"TOPLEFT"}, {120, 0, 100, 18}, "^7DEFAULT", function() self:SetColor("^7") end)
3334

34-
self.controls.edit = new("EditControl", {"TOPLEFT",self.controls.fire,"TOPLEFT"}, {0, 48, 0, 0}, "", nil, "^%C\t\n", nil, nil, 16, true)
35+
self.controls.edit = new("EditControl", {"TOPLEFT",self.controls.fire,"TOPLEFT"}, {0, 48, 0, 0}, "", nil, "^%C\t\n", nil, function()
36+
self.controls.edit:RefreshSearch()
37+
end, 16, true)
3538
self.controls.edit.width = function()
3639
return self.width - 16
3740
end
3841
self.controls.edit.height = function()
3942
return self.height - 128
4043
end
41-
self.controls.toggleColorCodes = new("ButtonControl", {"TOPRIGHT",self,"TOPRIGHT"}, {-10, 70, 160, 20}, "Show Color Codes", function()
44+
45+
self.controls.searchClear = new("ButtonControl", {"TOPRIGHT",self,"TOPRIGHT"}, {-10, 10, 20, 20}, "x", function()
46+
self:ClearSearch()
47+
end)
48+
self.controls.searchClear.tooltipText = function()
49+
return "Clear search"
50+
end
51+
self.controls.searchClear.enabled = function()
52+
return self.controls.search.buf ~= ""
53+
end
54+
self.controls.searchNext = new("ButtonControl", {"RIGHT",self.controls.searchClear,"LEFT"}, {-4, 0, 24, 20}, "\\/", function()
55+
self:AdvanceSearch(1)
56+
end)
57+
self.controls.searchNext.tooltipText = function()
58+
return "Next match\n\nShortcut: Enter"
59+
end
60+
self.controls.searchNext.enabled = function()
61+
return #self.controls.edit.searchMatches > 0
62+
end
63+
self.controls.searchPrev = new("ButtonControl", {"RIGHT",self.controls.searchNext,"LEFT"}, {-4, 0, 24, 20}, "/\\", function()
64+
self:AdvanceSearch(-1)
65+
end)
66+
self.controls.searchPrev.tooltipText = function()
67+
return "Previous match\n\nShortcut: Shift+Enter"
68+
end
69+
self.controls.searchPrev.enabled = function()
70+
return #self.controls.edit.searchMatches > 0
71+
end
72+
self.controls.search = new("EditControl", {"RIGHT",self.controls.searchPrev,"LEFT"}, {-8, 0, 220, 20}, "", nil, "%c", 100, function(buf)
73+
self.controls.edit:SetSearchQuery(buf, true)
74+
end)
75+
self.controls.search.width = function()
76+
local baseWidth = math.max(140, math.min(320, self.width - 700))
77+
return math.max(100, m_floor(baseWidth * 0.7))
78+
end
79+
self.controls.search.enterFunc = function()
80+
self:AdvanceSearch(IsKeyDown("SHIFT") and -1 or 1)
81+
end
82+
self.controls.search:SetPlaceholder("Search")
83+
84+
self.controls.searchCount = new("LabelControl", {"RIGHT",self.controls.search,"LEFT"}, {-8, 0, 60, 16}, function()
85+
if self.controls.search.buf == "" then
86+
return ""
87+
end
88+
local matchCount = #self.controls.edit.searchMatches
89+
if matchCount == 0 then
90+
return "^10/0"
91+
end
92+
return ("^7%d/%d"):format(self.controls.edit.searchFocusIndex or 0, matchCount)
93+
end)
94+
self.controls.searchCount.x = function()
95+
local reservedWidth = self.controls.searchCount:GetProperty("width")
96+
local labelWidth = DrawStringWidth(self.controls.searchCount:GetProperty("height"), "VAR", self.controls.searchCount:GetProperty("label"))
97+
return reservedWidth - 8 - labelWidth
98+
end
99+
self.controls.searchCount.width = function()
100+
return DrawStringWidth(self.controls.searchCount:GetProperty("height"), "VAR", "^7999/9999") + 8
101+
end
102+
103+
self.controls.toggleColorCodes = new("ButtonControl", {"TOPRIGHT",self,"TOPRIGHT"}, {-10, 38, 160, 20}, "Show Color Codes", function()
42104
self.showColorCodes = not self.showColorCodes
43105
self:SetShowColorCodes(self.showColorCodes)
44106
end)
@@ -54,6 +116,7 @@ function NotesTabClass:SetShowColorCodes(setting)
54116
self.controls.toggleColorCodes.label = "Show Color Codes"
55117
self.controls.edit.buf = self.controls.edit.buf:gsub("%^_x(%x%x%x%x%x%x)","^x%1"):gsub("%^_(%d)","^%1")
56118
end
119+
self.controls.edit:RefreshSearch(#self.controls.edit.searchMatches > 0)
57120
end
58121

59122
function NotesTabClass:SetColor(color)
@@ -82,6 +145,19 @@ function NotesTabClass:Save(xml)
82145
self.lastContent = self.controls.edit.buf
83146
end
84147

148+
function NotesTabClass:ClearSearch()
149+
self.controls.search:SetText("", true)
150+
self.controls.search:SelectAll()
151+
self:SelectControl(self.controls.search)
152+
return self.controls.search
153+
end
154+
155+
function NotesTabClass:AdvanceSearch(direction)
156+
self.controls.edit:AdvanceSearchMatch(direction)
157+
self:SelectControl(self.controls.search)
158+
return self.controls.search
159+
end
160+
85161
function NotesTabClass:Draw(viewPort, inputEvents)
86162
self.x = viewPort.x
87163
self.y = viewPort.y
@@ -94,6 +170,17 @@ function NotesTabClass:Draw(viewPort, inputEvents)
94170
self.controls.edit:Undo()
95171
elseif event.key == "y" and IsKeyDown("CTRL") then
96172
self.controls.edit:Redo()
173+
elseif event.key == "f" and IsKeyDown("CTRL") then
174+
self:SelectControl(self.controls.search)
175+
self.controls.search:SelectAll()
176+
inputEvents[id] = nil
177+
elseif event.key == "ESCAPE" and self.controls.search.hasFocus then
178+
if self.controls.search.buf ~= "" then
179+
self:ClearSearch()
180+
else
181+
self:SelectControl(self.controls.edit)
182+
end
183+
inputEvents[id] = nil
97184
end
98185
end
99186
end

0 commit comments

Comments
 (0)