Skip to content

Commit

Permalink
Merge pull request #5201 from wiktor-obrebski/widgets/edit-field-to-t…
Browse files Browse the repository at this point in the history
…ext-area

Widgets/edit field to text area
  • Loading branch information
myk002 authored Jan 20, 2025
2 parents 239421e + 93845d1 commit f5e17bb
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 198 deletions.
245 changes: 116 additions & 129 deletions library/lua/gui/widgets/edit_field.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,61 @@ 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 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,
key = DEFAULT_NIL,
on_submit = DEFAULT_NIL,
on_submit2 = 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:getText()) then
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

function TextFieldArea:getPreferredFocusState()
-- allow EditField to manage focus
return false
end

----------------
-- Edit field --
----------------
Expand Down Expand Up @@ -52,179 +104,114 @@ end

function EditField:init()
local function on_activate()
self.saved_text = self.text
self:setFocus(true)
end

self.start_pos = 1
self.cursor = #self.text + 1
self.ignore_keys = self.ignore_keys or {}

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},
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,
on_focus = self:callback('onFocus'),
on_unfocus = self.on_unfocus,
ignore_keys={
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()
end

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}}
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

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)
text = text or ''

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
local old = self.text
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)
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
-- only react to our hotkey
if not self.text_area.focus then
return self:inputToSubviews(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)
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
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
if self.text_area:onInput(keys) then
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())
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
Expand Down
40 changes: 32 additions & 8 deletions library/lua/gui/widgets/text_area.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ TextArea.ATTRS{

function TextArea:init()
self.render_start_line_y = 1
self.render_start_x = 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,
Expand All @@ -35,7 +36,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
Expand Down Expand Up @@ -85,12 +89,25 @@ 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

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
Expand Down Expand Up @@ -164,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
Expand All @@ -177,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

Expand Down
Loading

0 comments on commit f5e17bb

Please sign in to comment.