From e0c9bfd26e08075d77c7d2d6768caaf6c8a74a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 19:26:44 +0200 Subject: [PATCH 01/13] Improve TextField widget logic so it use richer TextArea widget --- library/lua/gui/widgets/edit_field.lua | 155 +++++++----------- library/lua/gui/widgets/text_area.lua | 2 +- .../widgets/text_area/text_area_content.lua | 5 + 3 files changed, 61 insertions(+), 101 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 8457b88f51..8d60838440 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -2,9 +2,25 @@ local gui = require('gui') local utils = require('utils') local Widget = require('gui.widgets.widget') local HotkeyLabel = require('gui.widgets.labels.hotkey_label') +local TextArea = require('gui.widgets.text_area') local getval = utils.getval +TextFieldArea = defclass(TextFieldArea, TextArea) +TextFieldArea.ATTRS{ + on_char = DEFAULT_NIL, + modal = false, +} + +function TextFieldArea:onInput(keys) + if self.on_char and keys._STRING and keys._STRING ~= 0 then + if not self.on_char(string.char(keys._STRING), self.text) then + return self.modal + end + end + return TextFieldArea.super.onInput(self, keys) +end + ---------------- -- Edit field -- ---------------- @@ -56,14 +72,32 @@ function EditField:init() self:setFocus(true) end + self.key = 'CUSTOM_N' + self.start_pos = 1 self.cursor = #self.text + 1 - self:addviews{HotkeyLabel{frame={t=0,l=0}, - key=self.key, - key_sep=self.key_sep, - label=self.label_text, - on_activate=self.key and on_activate or nil}} + self.label = HotkeyLabel{ + frame={t=0,l=0}, + key=self.key, + key_sep=self.key_sep, + label=self.label_text, + on_activate=self.key and on_activate or nil + } + self.text_area = TextFieldArea{ + one_line_mode=true, + frame={t=0,r=0}, + text={self.text or ''}, + text_pen=self.text_pen or COLOR_LIGHTCYAN, + modal=self.modal, + on_char=self.on_char, + ignore_keys={'SELECT', 'SELECT_ALL'}, + on_text_change=self:callback('onTextAreaTextChange'), + on_cursor_change=function(cursor) self.cursor = cursor end + } + + self:addviews{self.label, self.text_area} + self.text_area.frame.l = self.label:getTextWidth() end function EditField:getPreferredFocusState() @@ -76,52 +110,31 @@ function EditField:setCursor(cursor) return end self.cursor = math.max(1, cursor) + self.text_area:setCursor(cursor) end function EditField:setText(text, cursor) local old = self.text self.text = text + self.text_area:setText(text) + self:setCursor(cursor) if self.on_change and text ~= old then self.on_change(self.text, old) end end -function EditField:postUpdateLayout() - self.text_offset = self.subviews[1]:getTextWidth() -end - ----@param dc gui.Painter -function EditField:onRenderBody(dc) - dc:pen(self.text_pen or COLOR_LIGHTCYAN) - - local cursor_char = '_' - if not getval(self.active) or not self.focus or gui.blink_visible(300) then - cursor_char = (self.cursor > #self.text) and ' ' or - self.text:sub(self.cursor, self.cursor) - end - local txt = self.text:sub(1, self.cursor - 1) .. cursor_char .. - self.text:sub(self.cursor + 1) - local max_width = dc.width - self.text_offset - self.start_pos = 1 - if #txt > max_width then - -- get the substring in the vicinity of the cursor - max_width = max_width - 2 - local half_width = math.floor(max_width/2) - local start_pos = math.max(1, self.cursor-half_width) - local end_pos = math.min(#txt, self.cursor+half_width-1) - if self.cursor + half_width > #txt then - start_pos = #txt - (max_width - 1) - end - if self.cursor - half_width <= 1 then - end_pos = max_width + 1 +function EditField:onTextAreaTextChange(text) + if self.text ~= text then + self.text = text + if self.on_change then + self.on_change(self.text, old) end - self.start_pos = start_pos > 1 and start_pos - 1 or start_pos - txt = ('%s%s%s'):format(start_pos == 1 and '' or string.char(27), - txt:sub(start_pos, end_pos), - end_pos == #txt and '' or string.char(26)) end - dc:advance(self.text_offset):string(txt) +end + +function EditField:setFocus(focus) + self.text_area:setFocus(focus) end function EditField:insert(text) @@ -132,8 +145,7 @@ end function EditField:onInput(keys) if not self.focus then - -- only react to our hotkey - return self:inputToSubviews(keys) + return self.label:onInput(keys) end if self.ignore_keys then @@ -164,67 +176,10 @@ function EditField:onInput(keys) end end return not not self.key - elseif keys.CUSTOM_DELETE then - local old = self.text - local del_pos = self.cursor - if del_pos <= #old then - self:setText(old:sub(1, del_pos-1) .. old:sub(del_pos+1), del_pos) - end - return true - elseif keys._STRING then - local old = self.text - if keys._STRING == 0 then - -- handle backspace - local del_pos = self.cursor - 1 - if del_pos > 0 then - self:setText(old:sub(1, del_pos-1) .. old:sub(del_pos+1), del_pos) - end - else - local cv = string.char(keys._STRING) - if not self.on_char or self.on_char(cv, old) then - self:insert(cv) - elseif self.on_char then - return self.modal - end - end - return true - elseif keys.KEYBOARD_CURSOR_LEFT then - self:setCursor(self.cursor - 1) - return true - elseif keys.CUSTOM_CTRL_LEFT then -- back one word - local _, prev_word_end = self.text:sub(1, self.cursor-1): - find('.*[%w_%-][^%w_%-]') - self:setCursor(prev_word_end or 1) - return true - elseif keys.CUSTOM_HOME then - self:setCursor(1) - return true - elseif keys.KEYBOARD_CURSOR_RIGHT then - self:setCursor(self.cursor + 1) - return true - elseif keys.CUSTOM_CTRL_RIGHT then -- forward one word - local _,next_word_start = self.text:find('[^%w_%-][%w_%-]', self.cursor) - self:setCursor(next_word_start) - return true - elseif keys.CUSTOM_END then - self:setCursor() - return true - elseif keys.CUSTOM_CTRL_C then - dfhack.internal.setClipboardTextCp437(self.text) - return true - elseif keys.CUSTOM_CTRL_X then - dfhack.internal.setClipboardTextCp437(self.text) - self:setText('') - return true - elseif keys.CUSTOM_CTRL_V then - self:insert(dfhack.internal.getClipboardTextCp437()) + end + + if EditField.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN then - local mouse_x = self:getMousePos() - if mouse_x then - self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) - return true - end end -- if we're modal, then unconditionally eat all the input diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index c76a7d03e8..564728caa2 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -25,7 +25,7 @@ function TextArea:init() self.render_start_line_y = 1 self.text_area = TextAreaContent{ - frame={l=0,r=3,t=0}, + frame={l=0,r=self.one_line_mode and 0 or 3,t=0}, text=self.init_text, text_pen=self.text_pen, diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index 48877ccaf7..ee692a58b9 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -68,6 +68,11 @@ function TextAreaContent:postComputeFrame() end function TextAreaContent:recomputeLines() + if not self.frame_body then + -- called before first layout compute + return + end + self.wrapped_text:update( self.text, -- something cursor '_' need to be add at the end of a line From a9716da8efb76ab04c4b1fbb2d5610587e0865ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 19:42:36 +0200 Subject: [PATCH 02/13] Polishing TextField ingored keys --- library/lua/gui/widgets/edit_field.lua | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 8d60838440..36609db1f7 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -72,10 +72,9 @@ function EditField:init() self:setFocus(true) end - self.key = 'CUSTOM_N' - self.start_pos = 1 self.cursor = #self.text + 1 + self.ignore_keys = self.ignore_keys or {} self.label = HotkeyLabel{ frame={t=0,l=0}, @@ -91,13 +90,23 @@ function EditField:init() text_pen=self.text_pen or COLOR_LIGHTCYAN, modal=self.modal, on_char=self.on_char, - ignore_keys={'SELECT', 'SELECT_ALL'}, + ignore_keys={ + 'SELECT', + 'SELECT_ALL', + 'KEYBOARD_CURSOR_UP', + 'KEYBOARD_CURSOR_DOWN', + table.unpack(self.ignore_keys) + }, on_text_change=self:callback('onTextAreaTextChange'), on_cursor_change=function(cursor) self.cursor = cursor end } self:addviews{self.label, self.text_area} self.text_area.frame.l = self.label:getTextWidth() + + if self.key then + self.text_area:setFocus(false) + end end function EditField:getPreferredFocusState() @@ -148,12 +157,6 @@ function EditField:onInput(keys) return self.label:onInput(keys) end - if self.ignore_keys then - for _,ignore_key in ipairs(self.ignore_keys) do - if keys[ignore_key] then return false end - end - end - if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then self:setText(self.saved_text) self:setFocus(false) From ca4fda53ee31792d937e1c279b3f163fa596e90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 19:52:54 +0200 Subject: [PATCH 03/13] Fix issues with TextField cursor --- library/lua/gui/widgets/edit_field.lua | 9 +++++---- library/lua/gui/widgets/text_area.lua | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 36609db1f7..913304814a 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -114,12 +114,13 @@ function EditField:getPreferredFocusState() end function EditField:setCursor(cursor) - if not cursor or cursor > #self.text then - self.cursor = #self.text + 1 - return + if not cursor then + cursor = #self.text + 1 end - self.cursor = math.max(1, cursor) + self.cursor = cursor + self.text_area:setCursor(cursor) + self.cursor = self.text_area:getCursor() end function EditField:setText(text, cursor) diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index 564728caa2..d28b3823ac 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -35,7 +35,10 @@ function TextArea:init() one_line_mode=self.one_line_mode, on_text_change=function (text, old_text) - self:updateLayout() + if self.frame_body then + self:updateLayout() + end + if self.on_text_change then self.on_text_change(text, old_text) end @@ -85,12 +88,14 @@ function TextArea:onCursorChange(cursor, old_cursor) self.text_area.cursor ) - if y >= self.render_start_line_y + self.text_area.frame_body.height then - self:updateScrollbar( - y - self.text_area.frame_body.height + 1 - ) - elseif (y < self.render_start_line_y) then - self:updateScrollbar(y) + if self.text_area.frame_body then + if y >= self.render_start_line_y + self.text_area.frame_body.height then + self:updateScrollbar( + y - self.text_area.frame_body.height + 1 + ) + elseif (y < self.render_start_line_y) then + self:updateScrollbar(y) + end end if self.on_cursor_change then From ee68d9f1e043c6eee51bad311a568058f0ada2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 21:24:01 +0200 Subject: [PATCH 04/13] Fix EditField issue with submitting results --- library/lua/gui/widgets/edit_field.lua | 57 +++++++++++-------- .../widgets/text_area/text_area_content.lua | 3 +- test/library/gui/widgets.EditField.lua | 33 ++++++----- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 913304814a..3d8faf27bf 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -9,6 +9,9 @@ local getval = utils.getval TextFieldArea = defclass(TextFieldArea, TextArea) TextFieldArea.ATTRS{ on_char = DEFAULT_NIL, + key = DEFAULT_NIL, + on_submit = DEFAULT_NIL, + on_submit2 = DEFAULT_NIL, modal = false, } @@ -18,6 +21,25 @@ function TextFieldArea:onInput(keys) return self.modal end end + + if keys.SELECT or keys.SELECT_ALL then + if self.key then + self:setFocus(false) + end + if keys.SELECT_ALL then + if self.on_submit2 then + self.on_submit2(self:getText()) + return true + end + else + if self.on_submit then + self.on_submit(self:getText()) + return true + end + end + return not not self.key + end + return TextFieldArea.super.onInput(self, keys) end @@ -86,13 +108,14 @@ function EditField:init() self.text_area = TextFieldArea{ one_line_mode=true, frame={t=0,r=0}, - text={self.text or ''}, + init_text=self.text or '', text_pen=self.text_pen or COLOR_LIGHTCYAN, modal=self.modal, on_char=self.on_char, + key = self.key, + on_submit = self.on_submit, + on_submit2 = self.on_submit2, ignore_keys={ - 'SELECT', - 'SELECT_ALL', 'KEYBOARD_CURSOR_UP', 'KEYBOARD_CURSOR_DOWN', table.unpack(self.ignore_keys) @@ -124,6 +147,8 @@ function EditField:setCursor(cursor) end function EditField:setText(text, cursor) + text = text or '' + local old = self.text self.text = text self.text_area:setText(text) @@ -149,12 +174,14 @@ end function EditField:insert(text) local old = self.text - self:setText(old:sub(1,self.cursor-1)..text..old:sub(self.cursor), - self.cursor + #text) + self:setText( + old:sub(1,self.cursor-1)..text..old:sub(self.cursor), + self.cursor + #text + ) end function EditField:onInput(keys) - if not self.focus then + if not self.text_area.focus then return self.label:onInput(keys) end @@ -164,24 +191,6 @@ function EditField:onInput(keys) return true end - if keys.SELECT or keys.SELECT_ALL then - if self.key then - self:setFocus(false) - end - if keys.SELECT_ALL then - if self.on_submit2 then - self.on_submit2(self.text) - return true - end - else - if self.on_submit then - self.on_submit(self.text) - return true - end - end - return not not self.key - end - if EditField.super.onInput(self, keys) then return true end diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index ee692a58b9..49710f09af 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -28,7 +28,7 @@ function TextAreaContent:init() self.clipboard_mode = CLIPBOARD_MODE.LOCAL self.render_start_line_y = 1 - self.cursor = nil + self.cursor = 1 self.main_pen = dfhack.pen.parse({ fg=self.text_pen, @@ -483,6 +483,7 @@ function TextAreaContent:onMouseInput(keys) elseif keys._MOUSE_L_DOWN then local mouse_x, mouse_y = self:getMousePos() + if mouse_x and mouse_y then if (self:getMultiLeftClick(mouse_x + 1, mouse_y + 1) > 1) then return true diff --git a/test/library/gui/widgets.EditField.lua b/test/library/gui/widgets.EditField.lua index 755c3c54de..4a25cc1ea6 100644 --- a/test/library/gui/widgets.EditField.lua +++ b/test/library/gui/widgets.EditField.lua @@ -29,11 +29,11 @@ function test.editfield_cursor() e:onInput{CUSTOM_HOME=true} expect.eq(1, e.cursor, 'cursor should be at beginning of string') e:onInput{CUSTOM_CTRL_RIGHT=true} - expect.eq(6, e.cursor, 'goto beginning of next word') + expect.eq(5, e.cursor, 'goto end of current word') e:onInput{CUSTOM_END=true} expect.eq(16, e.cursor, 'cursor should be at end of string') e:onInput{CUSTOM_CTRL_LEFT=true} - expect.eq(9, e.cursor, 'goto end of previous word') + expect.eq(10, e.cursor, 'goto beginning of current word') end function test.editfield_click() @@ -41,20 +41,25 @@ function test.editfield_click() e:setFocus(true) expect.eq(5, e.cursor) - mock.patch(e, 'getMousePos', mock.func(0), function() - e:onInput{_MOUSE_L_DOWN=true} - expect.eq(1, e.cursor) - end) + local text_area_content = e.text_area.text_area - mock.patch(e, 'getMousePos', mock.func(20), function() - e:onInput{_MOUSE_L_DOWN=true} - expect.eq(5, e.cursor, 'should only seek to end of text') - end) + mock.patch(text_area_content, 'getMousePos', mock.func(0, 0), function() + e:onInput{_MOUSE_L_DOWN=true, _MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} + expect.eq(1, e.cursor) + end) - mock.patch(e, 'getMousePos', mock.func(2), function() - e:onInput{_MOUSE_L_DOWN=true} - expect.eq(3, e.cursor) - end) + mock.patch(text_area_content, 'getMousePos', mock.func(20, 0), function() + e:onInput{_MOUSE_L_DOWN=true, _MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} + expect.eq(5, e.cursor, 'should only seek to end of text') + end) + + mock.patch(text_area_content, 'getMousePos', mock.func(2, 0), function() + e:onInput{_MOUSE_L_DOWN=true, _MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} + expect.eq(3, e.cursor) + end) end function test.editfield_ignore_keys() From 8ee053a9df427c274123c39ed603a8e61f403287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 22:00:00 +0200 Subject: [PATCH 05/13] Fix journal and textarea tests --- library/lua/gui/widgets/text_area/text_area_content.lua | 2 +- test/library/gui/widgets.EditField.lua | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index 49710f09af..a8f716866b 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -28,7 +28,7 @@ function TextAreaContent:init() self.clipboard_mode = CLIPBOARD_MODE.LOCAL self.render_start_line_y = 1 - self.cursor = 1 + self.cursor = nil self.main_pen = dfhack.pen.parse({ fg=self.text_pen, diff --git a/test/library/gui/widgets.EditField.lua b/test/library/gui/widgets.EditField.lua index 4a25cc1ea6..432b65a163 100644 --- a/test/library/gui/widgets.EditField.lua +++ b/test/library/gui/widgets.EditField.lua @@ -4,6 +4,9 @@ local widgets = require('gui.widgets') function test.editfield_cursor() local e = widgets.EditField{} + + -- cursor is normally set in `postUpdateLayout`, hard to test in unit tests + e:setCursor(1) e:setFocus(true) expect.eq(1, e.cursor, 'cursor should be after the empty string') @@ -64,6 +67,8 @@ end function test.editfield_ignore_keys() local e = widgets.EditField{ignore_keys={'CUSTOM_B', 'CUSTOM_C'}} + -- cursor is normally set in `postUpdateLayout`, hard to test in unit tests + e:setCursor(1) e:setFocus(true) e:onInput{_STRING=string.byte('a'), CUSTOM_A=true} From 7f8cf7d9f51cc81bdf916e74ad069a9a87a8b092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 14 Jan 2025 07:57:50 +0100 Subject: [PATCH 06/13] Add cp437 10 (new line) char rendering to TextArea one line mode --- library/lua/gui/widgets/edit_field.lua | 4 +- .../widgets/text_area/text_area_content.lua | 61 ++++++++++++------- test/library/gui/widgets.TextArea.lua | 25 +++++++- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 3d8faf27bf..f538df6691 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -17,7 +17,7 @@ TextFieldArea.ATTRS{ function TextFieldArea:onInput(keys) if self.on_char and keys._STRING and keys._STRING ~= 0 then - if not self.on_char(string.char(keys._STRING), self.text) then + if not self.on_char(string.char(keys._STRING), self.text) then return self.modal end end @@ -26,6 +26,7 @@ function TextFieldArea:onInput(keys) if self.key then self:setFocus(false) end + if keys.SELECT_ALL then if self.on_submit2 then self.on_submit2(self:getText()) @@ -37,6 +38,7 @@ function TextFieldArea:onInput(keys) return true end end + return not not self.key end diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index a8f716866b..fea4d900c2 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -7,6 +7,22 @@ local HistoryStore = require('gui.widgets.text_area.history_store') local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} local HISTORY_ENTRY = HistoryStore.HISTORY_ENTRY + +PassthroughText = defclass(PassthroughText) + +function PassthroughText:init() +end + +function PassthroughText:update(text) + self.lines = {text} +end +function PassthroughText:coordsToIndex(x, y) + return x or 1 +end +function PassthroughText:indexToCoords(index) + return index or 1, 1 +end + TextAreaContent = defclass(TextAreaContent, Widget) TextAreaContent.ATTRS{ @@ -41,9 +57,8 @@ function TextAreaContent:init() bold=true }) - self.text = self:normalizeText(self.text) - - self.wrapped_text = WrappedText{ + local TextWrapper = self.one_line_mode and PassthroughText or WrappedText + self.wrapped_text = TextWrapper{ text=self.text, wrap_width=256 } @@ -51,14 +66,6 @@ function TextAreaContent:init() self.history = HistoryStore{history_size=self.history_size} end -function TextAreaContent:normalizeText(text) - if self.one_line_mode then - return text:gsub("\r?\n", "") - end - - return text -end - function TextAreaContent:setRenderStartLineY(render_start_line_y) self.render_start_line_y = render_start_line_y end @@ -192,7 +199,7 @@ end function TextAreaContent:setText(text) local old_text = self.text - self.text = self:normalizeText(text) + self.text = text self:recomputeLines() @@ -212,11 +219,19 @@ function TextAreaContent:insert(text) self:setCursor(self.cursor + #text) end +function TextAreaContent:normalizeLine(text_line) + if self.one_line_mode or self.debug then + return text_line + else + -- do not render new lines symbol in multiline mode + return text_line:gsub(NEWLINE, '') + end +end + function TextAreaContent:onRenderBody(dc) dc:pen(self.main_pen) local max_width = dc.width - local new_line = self.debug and NEWLINE or '' local lines_to_render = math.min( dc.height, @@ -225,8 +240,7 @@ function TextAreaContent:onRenderBody(dc) dc:seek(0, self.render_start_line_y - 1) for i = self.render_start_line_y, self.render_start_line_y + lines_to_render - 1 do - -- do not render new lines symbol - local line = self.wrapped_text.lines[i]:gsub(NEWLINE, new_line) + local line = self:normalizeLine(self.wrapped_text.lines[i]) dc:string(line) dc:newline() end @@ -245,7 +259,6 @@ function TextAreaContent:onRenderBody(dc) end if self:hasSelection() then - local sel_new_line = self.debug and PERIOD or '' local from, to = self.cursor, self.sel_end if (from > to) then from, to = to, from @@ -254,24 +267,26 @@ function TextAreaContent:onRenderBody(dc) local from_x, from_y = self.wrapped_text:indexToCoords(from) local to_x, to_y = self.wrapped_text:indexToCoords(to) - local line = self.wrapped_text.lines[from_y] - :sub(from_x, to_y == from_y and to_x or nil) - :gsub(NEWLINE, sel_new_line) + local line = self:normalizeLine( + self.wrapped_text.lines[from_y]:sub( + from_x, to_y == from_y and to_x or nil + ) + ) dc:pen(self.sel_pen) :seek(from_x - 1, from_y - 1) :string(line) for y = from_y + 1, to_y - 1 do - line = self.wrapped_text.lines[y]:gsub(NEWLINE, sel_new_line) + line = self:normalizeLine(self.wrapped_text.lines[y]) dc:seek(0, y - 1) :string(line) end if (to_y > from_y) then - local line = self.wrapped_text.lines[to_y] - :sub(1, to_x) - :gsub(NEWLINE, sel_new_line) + local line = self:normalizeLine( + self.wrapped_text.lines[to_y]:sub(1, to_x) + ) dc:seek(0, to_y - 1) :string(line) end diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 0223b05282..af5a538a89 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -66,8 +66,8 @@ local function arrange_textarea(options) if options.w then local border_width = 2 - local scrollbar_width = 3 - local cursor_buffor = 1 + local scrollbar_width = options.one_line_mode and 0 or 3 + local cursor_buffor = options.one_line_mode and 0 or 1 window_width = options.w + border_width + scrollbar_width + cursor_buffor end @@ -90,6 +90,7 @@ local function arrange_textarea(options) init_text=options.text or '', init_cursor=options.cursor or 1, frame={l=0,r=0,t=0,b=0}, + one_line_mode=options.one_line_mode, on_cursor_change=options.on_cursor_change, on_text_change=options.on_text_change, } @@ -3315,3 +3316,23 @@ function test.clear_undo_redo_history() screen:dismiss() end + +function test.render_new_lines_in_one_line_mode() + local text_area, screen, window, widget = arrange_textarea({ + w=80, + one_line_mode=true + }) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Pellentesque dignissim volutpat orci, sed molestie metus elementum vel.', + 'Donec sit amet mattis ligula, ac vestibulum lorem.', + }, '\n') + + widget:setText(text) + widget:setCursor(1) + + expect.eq(read_rendered_text(text_area), '_' .. text:sub(2, 80)) + + screen:dismiss() +end From 097e7551500b773a6925d60d80b798bdcb672033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 14 Jan 2025 09:00:48 +0100 Subject: [PATCH 07/13] Add horizontal offset control to one line mode TextArea --- library/lua/gui/widgets/text_area.lua | 15 +++++++++++++++ .../gui/widgets/text_area/text_area_content.lua | 16 +++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index d28b3823ac..67396d154c 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -23,6 +23,7 @@ TextArea.ATTRS{ function TextArea:init() self.render_start_line_y = 1 + self.render_start_x = 1 self.text_area = TextAreaContent{ frame={l=0,r=self.one_line_mode and 0 or 3,t=0}, @@ -96,6 +97,17 @@ function TextArea:onCursorChange(cursor, old_cursor) elseif (y < self.render_start_line_y) then self:updateScrollbar(y) end + + if self.one_line_mode then + local x_screen_offset_right = math.max(0, x - self.render_start_x - self.text_area.frame_body.width + 1) + local x_screen_offset_left = math.max(0, self.render_start_x - x) + + if x_screen_offset_right > 0 then + self.render_start_x = self.render_start_x + x_screen_offset_right + elseif x_screen_offset_left > 0 then + self.render_start_x = self.render_start_x - x_screen_offset_left + end + end end if self.on_cursor_change then @@ -169,6 +181,9 @@ end function TextArea:renderSubviews(dc) self.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) + if self.one_line_mode then + self.text_area.frame_body.x1 = self.frame_body.x1-(self.render_start_x - 1) + end -- only visible lines of text_area will be rendered TextArea.super.renderSubviews(self, dc) end diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index fea4d900c2..ef28811fb3 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -7,21 +7,11 @@ local HistoryStore = require('gui.widgets.text_area.history_store') local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} local HISTORY_ENTRY = HistoryStore.HISTORY_ENTRY +OneLineWrappedText = defclass(OneLineWrappedText, WrappedText) -PassthroughText = defclass(PassthroughText) - -function PassthroughText:init() -end - -function PassthroughText:update(text) +function OneLineWrappedText:update(text) self.lines = {text} end -function PassthroughText:coordsToIndex(x, y) - return x or 1 -end -function PassthroughText:indexToCoords(index) - return index or 1, 1 -end TextAreaContent = defclass(TextAreaContent, Widget) @@ -57,7 +47,7 @@ function TextAreaContent:init() bold=true }) - local TextWrapper = self.one_line_mode and PassthroughText or WrappedText + local TextWrapper = self.one_line_mode and OneLineWrappedText or WrappedText self.wrapped_text = TextWrapper{ text=self.text, wrap_width=256 From de678b1d04d115935785546ee8d6543fd3bd30fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 15 Jan 2025 07:45:48 +0100 Subject: [PATCH 08/13] Add test for TextArea one line mode --- library/lua/gui/widgets/edit_field.lua | 7 ++ .../widgets/text_area/text_area_content.lua | 11 +-- test/library/gui/widgets.TextArea.lua | 72 +++++++++++++++++-- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index f538df6691..8d389e72f8 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -3,9 +3,16 @@ local utils = require('utils') local Widget = require('gui.widgets.widget') local HotkeyLabel = require('gui.widgets.labels.hotkey_label') local TextArea = require('gui.widgets.text_area') +local WrappedText = require('gui.widgets.text_area.wrapped_text') local getval = utils.getval +OneLineWrappedText = defclass(OneLineWrappedText, WrappedText) + +function OneLineWrappedText:update(text) + self.lines = {text} +end + TextFieldArea = defclass(TextFieldArea, TextArea) TextFieldArea.ATTRS{ on_char = DEFAULT_NIL, diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index ef28811fb3..53573c06a0 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -570,12 +570,13 @@ end function TextAreaContent:onTextManipulationInput(keys) if keys.SELECT then -- handle enter + self.history:store( + HISTORY_ENTRY.WHITESPACE_BLOCK, + self.text, + self.cursor + ) + if not self.one_line_mode then - self.history:store( - HISTORY_ENTRY.WHITESPACE_BLOCK, - self.text, - self.cursor - ) self:insert(NEWLINE) end diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index af5a538a89..230fc675a2 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -3,6 +3,8 @@ local widgets = require('gui.widgets') config.target = 'core' +local CP437_NEW_LINE = '◙' + local function simulate_input_keys(...) local keys = {...} for _,key in ipairs(keys) do @@ -124,7 +126,7 @@ local function read_rendered_text(text_area) if pen == nil or pen.ch == nil or pen.ch == 0 or pen.fg == 0 then break else - text = text .. string.char(pen.ch) + text = text .. (pen.ch == 10 and CP437_NEW_LINE or string.char(pen.ch)) end end @@ -3323,16 +3325,74 @@ function test.render_new_lines_in_one_line_mode() one_line_mode=true }) - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Pellentesque dignissim volutpat orci, sed molestie metus elementum vel.', - 'Donec sit amet mattis ligula, ac vestibulum lorem.', - }, '\n') + local text_table = { + 'Lorem ipsum dolor sit amet, ', + 'consectetur adipiscing elit.', + } + + widget:setText(table.concat(text_table, '\n')) + + widget:setCursor(1) + + expect.eq(read_rendered_text(text_area), '_' .. table.concat(text_table, CP437_NEW_LINE):sub(2)) + + widget:setText('') + simulate_input_text(' test') + simulate_input_text('\n') + simulate_input_text(' test') + + expect.eq( + read_rendered_text(text_area), + ' test' .. CP437_NEW_LINE .. ' test' .. '_' + ) + + screen:dismiss() +end + +function test.should_ignore_submit_in_one_line_mode() + local text_area, screen, window, widget = arrange_textarea({ + w=80, + one_line_mode=true + }) + + local text = 'Lorem ipsum dolor sit amet' + + widget:setText(text) + + widget:setCursor(1) + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), '_' .. text:sub(2)) + + screen:dismiss() +end + +function test.should_scroll_horizontally_in_one_line_mode() + local text_area, screen, window, widget = arrange_textarea({ + w=80, + one_line_mode=true + }) + + local text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque dignissim volutpat orci, sed' widget:setText(text) + widget:setCursor(1) expect.eq(read_rendered_text(text_area), '_' .. text:sub(2, 80)) + widget:setCursor(81) + + expect.eq(read_rendered_text(text_area), text:sub(2, 80) .. '_') + + widget:setCursor(90) + + expect.eq(read_rendered_text(text_area), text:sub(11, 89) .. '_') + + widget:setCursor(2) + + expect.eq(read_rendered_text(text_area), '_' .. text:sub(3, 81)) + screen:dismiss() end From 3695606e44e187adfc9e0bdbec6f22c726cc1387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 19 Jan 2025 08:57:54 +0100 Subject: [PATCH 09/13] Fix EditField cursor behaviour --- library/lua/gui/widgets/edit_field.lua | 22 ++++++++++++---------- library/lua/gui/widgets/text_area.lua | 4 ++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 8d389e72f8..abe6051ca5 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -24,7 +24,7 @@ TextFieldArea.ATTRS{ function TextFieldArea:onInput(keys) if self.on_char and keys._STRING and keys._STRING ~= 0 then - if not self.on_char(string.char(keys._STRING), self.text) then + if not self.on_char(string.char(keys._STRING), self:getText()) then return self.modal end end @@ -52,6 +52,11 @@ function TextFieldArea:onInput(keys) return TextFieldArea.super.onInput(self, keys) end +function TextFieldArea:getPreferredFocusState() + -- allow EditField to manage focus + return false +end + ---------------- -- Edit field -- ---------------- @@ -124,6 +129,8 @@ function EditField:init() key = self.key, on_submit = self.on_submit, on_submit2 = self.on_submit2, + on_focus = self.on_focus, + on_unfocus = self.on_unfocus, ignore_keys={ 'KEYBOARD_CURSOR_UP', 'KEYBOARD_CURSOR_DOWN', @@ -134,11 +141,8 @@ function EditField:init() } self:addviews{self.label, self.text_area} - self.text_area.frame.l = self.label:getTextWidth() - if self.key then - self.text_area:setFocus(false) - end + self.text_area.frame.l = self.label:getTextWidth() end function EditField:getPreferredFocusState() @@ -170,6 +174,7 @@ end function EditField:onTextAreaTextChange(text) if self.text ~= text then + local old = self.text self.text = text if self.on_change then self.on_change(self.text, old) @@ -190,11 +195,8 @@ function EditField:insert(text) end function EditField:onInput(keys) - if not self.text_area.focus then - return self.label:onInput(keys) - end - - if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then + if self.text_area.focus and self.key and + (keys.LEAVESCREEN or keys._MOUSE_R) then self:setText(self.saved_text) self:setFocus(false) return true diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index 67396d154c..75d83fef43 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -197,6 +197,10 @@ function TextArea:onInput(keys) self:setFocus(true) end + if not self.focus then + return false + end + return TextArea.super.onInput(self, keys) end From 8e3a79e7537f426dc3f0ec0e0d9ee90cef6c0965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 19 Jan 2025 10:36:20 +0100 Subject: [PATCH 10/13] Fix broken 2/8 keys in EditField TextArea should not consume up/down chars in one line mode --- library/lua/gui/widgets/edit_field.lua | 2 -- library/lua/gui/widgets/text_area/text_area_content.lua | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index abe6051ca5..9cde2cef62 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -132,8 +132,6 @@ function EditField:init() on_focus = self.on_focus, on_unfocus = self.on_unfocus, ignore_keys={ - 'KEYBOARD_CURSOR_UP', - 'KEYBOARD_CURSOR_DOWN', table.unpack(self.ignore_keys) }, on_text_change=self:callback('onTextAreaTextChange'), diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index 53573c06a0..da18daad34 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -517,7 +517,7 @@ function TextAreaContent:onCursorInput(keys) elseif keys.KEYBOARD_CURSOR_RIGHT then self:setCursor(self.cursor + 1) return true - elseif keys.KEYBOARD_CURSOR_UP then + elseif keys.KEYBOARD_CURSOR_UP and not self.one_line_mode then local x, y = self.wrapped_text:indexToCoords(self.cursor) local last_cursor_x = self.last_cursor_x or x local offset = y > 1 and @@ -526,7 +526,7 @@ function TextAreaContent:onCursorInput(keys) self:setCursor(offset) self.last_cursor_x = last_cursor_x return true - elseif keys.KEYBOARD_CURSOR_DOWN then + elseif keys.KEYBOARD_CURSOR_DOWN and not self.one_line_mode then local x, y = self.wrapped_text:indexToCoords(self.cursor) local last_cursor_x = self.last_cursor_x or x local offset = y < #self.wrapped_text.lines and From 7d8d191523f92a499d943e15b2066226538115e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 19 Jan 2025 11:04:43 +0100 Subject: [PATCH 11/13] Standarize behaviour of TextArea left/right keys when selection exists --- .../gui/widgets/text_area/text_area_content.lua | 16 +++++++++++++--- test/library/gui/widgets.TextArea.lua | 17 ++++++----------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index da18daad34..0791f200d9 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -98,7 +98,7 @@ function TextAreaContent:setCursor(cursor_offset) end function TextAreaContent:setSelection(from_offset, to_offset) - -- text selection is always start on self.cursor and on self.sel_end + -- text selection is always start on self.cursor and end on self.sel_end self:setCursor(from_offset) self.sel_end = to_offset @@ -512,10 +512,20 @@ end function TextAreaContent:onCursorInput(keys) if keys.KEYBOARD_CURSOR_LEFT then - self:setCursor(self.cursor - 1) + if self:hasSelection() then + self:setCursor(math.min(self.cursor, self.sel_end)) + else + self:setCursor(self.cursor - 1) + end + return true elseif keys.KEYBOARD_CURSOR_RIGHT then - self:setCursor(self.cursor + 1) + if self:hasSelection() then + self:setCursor(math.max(self.cursor, self.sel_end)) + else + self:setCursor(self.cursor + 1) + end + return true elseif keys.KEYBOARD_CURSOR_UP and not self.one_line_mode then local x, y = self.wrapped_text:indexToCoords(self.cursor) diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 230fc675a2..8212763237 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -1718,32 +1718,27 @@ function test.arrows_reset_selection() local text = table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', }, '\n') simulate_input_text(text) simulate_input_keys('CUSTOM_CTRL_A') - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', - }, '\n')); + expect.eq(read_rendered_text(text_area), text .. '_'); - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); + expect.eq(read_selected_text(text_area), text); simulate_input_keys('KEYBOARD_CURSOR_LEFT') expect.eq(read_selected_text(text_area), '') + expect.eq(read_rendered_text(text_area), '_' .. text:sub(2)) simulate_input_keys('CUSTOM_CTRL_A') simulate_input_keys('KEYBOARD_CURSOR_RIGHT') expect.eq(read_selected_text(text_area), '') + expect.eq(read_rendered_text(text_area), text:sub(1, #text) .. '_') simulate_input_keys('CUSTOM_CTRL_A') From b1e218543a97be2de35e6175a92737b749a7aaa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 20 Jan 2025 07:33:50 +0100 Subject: [PATCH 12/13] Fix EditField cancel behaviour --- library/lua/gui/widgets/edit_field.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 9cde2cef62..b3399d98f1 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -104,7 +104,6 @@ end function EditField:init() local function on_activate() - self.saved_text = self.text self:setFocus(true) end @@ -129,7 +128,7 @@ function EditField:init() key = self.key, on_submit = self.on_submit, on_submit2 = self.on_submit2, - on_focus = self.on_focus, + on_focus = self:callback('onFocus'), on_unfocus = self.on_unfocus, ignore_keys={ table.unpack(self.ignore_keys) @@ -143,6 +142,14 @@ function EditField:init() self.text_area.frame.l = self.label:getTextWidth() end +function EditField:onFocus() + self.saved_text = self.text + + if self.on_focus then + self:on_focus() + end +end + function EditField:getPreferredFocusState() return not self.key end From 93845d1b3f6c19edeaa2bb226e3c8d7846fd9dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 20 Jan 2025 07:58:13 +0100 Subject: [PATCH 13/13] Fix EditField eating Hotkey --- library/lua/gui/widgets/edit_field.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index b3399d98f1..203a2aa78a 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -200,14 +200,17 @@ function EditField:insert(text) end function EditField:onInput(keys) - if self.text_area.focus and self.key and - (keys.LEAVESCREEN or keys._MOUSE_R) then + if not self.text_area.focus then + return self:inputToSubviews(keys) + end + + if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then self:setText(self.saved_text) self:setFocus(false) return true end - if EditField.super.onInput(self, keys) then + if self.text_area:onInput(keys) then return true end