-
Notifications
You must be signed in to change notification settings - Fork 475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Widgets/text area #4995
base: develop
Are you sure you want to change the base?
Widgets/text area #4995
Changes from all commits
8bc9da2
55ae723
e237387
83a9a19
3ce4f1f
fa68bbe
cb549ed
ff41a5e
2ed6dcb
58c8d9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
-- Multiline text area control | ||
|
||
local Panel = require('gui.widgets.containers.panel') | ||
local Scrollbar = require('gui.widgets.scrollbar') | ||
local TextAreaContent = require('gui.widgets.text_area.text_area_content') | ||
local HistoryStore = require('gui.widgets.text_area.history_store') | ||
|
||
local HISTORY_ENTRY = HistoryStore.HISTORY_ENTRY | ||
|
||
TextArea = defclass(TextArea, Panel) | ||
|
||
TextArea.ATTRS{ | ||
init_text = '', | ||
init_cursor = DEFAULT_NIL, | ||
text_pen = COLOR_LIGHTCYAN, | ||
ignore_keys = {'STRING_A096'}, | ||
select_pen = COLOR_CYAN, | ||
on_text_change = DEFAULT_NIL, | ||
on_cursor_change = DEFAULT_NIL, | ||
one_line_mode = false, | ||
debug = false | ||
} | ||
|
||
function TextArea:init() | ||
self.render_start_line_y = 1 | ||
|
||
self.text_area = TextAreaContent{ | ||
frame={l=0,r=3,t=0}, | ||
text=self.init_text, | ||
|
||
text_pen=self.text_pen, | ||
ignore_keys=self.ignore_keys, | ||
select_pen=self.select_pen, | ||
debug=self.debug, | ||
one_line_mode=self.one_line_mode, | ||
|
||
on_text_change=function (val) | ||
self:updateLayout() | ||
if self.on_text_change then | ||
self.on_text_change(val) | ||
end | ||
end, | ||
on_cursor_change=self:callback('onCursorChange') | ||
} | ||
self.scrollbar = Scrollbar{ | ||
frame={r=0,t=1}, | ||
on_scroll=self:callback('onScrollbar'), | ||
visible=not self.one_line_mode | ||
} | ||
|
||
self:addviews{ | ||
self.text_area, | ||
self.scrollbar, | ||
} | ||
self:setFocus(true) | ||
end | ||
|
||
function TextArea:getText() | ||
return self.text_area.text | ||
end | ||
|
||
function TextArea:setText(text) | ||
self.text_area.history:store( | ||
HISTORY_ENTRY.OTHER, | ||
self:getText(), | ||
self:getCursor() | ||
) | ||
|
||
return self.text_area:setText(text) | ||
end | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we expose a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we make it so that if you call Edit: I repulled just to make sure so I didn't waste your time and confirmed that it's not working this way on the latest update. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have mixed feelings about it, can you present a usecase where it would be useful? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The situation where this came up for me is with this UI in a script I've been working on: I'm using this to be able to quickly add notes to individual units. Once I no longer need the note for the unit, I'll clear it out with ALT+C. I know I could CTRL+A/backspace to clear all but I also wanted to provide a shortcut to clear the notes with a single hotkey press (I had this before while using an EditField here and still find it handy). If a user ever did ALT+C by accident here, ideally they could CTRL+Z it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok. I would additionally expose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
function TextArea:getCursor() | ||
return self.text_area.cursor | ||
end | ||
|
||
function TextArea:setCursor(cursor_offset) | ||
return self.text_area:setCursor(cursor_offset) | ||
end | ||
|
||
function TextArea:clearHistory() | ||
return self.text_area.history:clear() | ||
end | ||
|
||
function TextArea:onCursorChange(cursor) | ||
local x, y = self.text_area.wrapped_text:indexToCoords( | ||
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) | ||
end | ||
|
||
if self.on_cursor_change then | ||
self.on_cursor_change(cursor) | ||
end | ||
end | ||
|
||
function TextArea:scrollToCursor(cursor_offset) | ||
if self.scrollbar.visible then | ||
local _, cursor_liny_y = self.text_area.wrapped_text:indexToCoords( | ||
cursor_offset | ||
) | ||
self:updateScrollbar(cursor_liny_y) | ||
end | ||
end | ||
|
||
function TextArea:getPreferredFocusState() | ||
return self.parent_view.focus | ||
end | ||
|
||
function TextArea:postUpdateLayout() | ||
self:updateScrollbar(self.render_start_line_y) | ||
|
||
if self.text_area.cursor == nil then | ||
local cursor = self.init_cursor or #self.init_text + 1 | ||
self.text_area:setCursor(cursor) | ||
self:scrollToCursor(cursor) | ||
end | ||
end | ||
|
||
function TextArea:onScrollbar(scroll_spec) | ||
local height = self.text_area.frame_body.height | ||
|
||
local render_start_line = self.render_start_line_y | ||
if scroll_spec == 'down_large' then | ||
render_start_line = render_start_line + math.ceil(height / 2) | ||
elseif scroll_spec == 'up_large' then | ||
render_start_line = render_start_line - math.ceil(height / 2) | ||
elseif scroll_spec == 'down_small' then | ||
render_start_line = render_start_line + 1 | ||
elseif scroll_spec == 'up_small' then | ||
render_start_line = render_start_line - 1 | ||
else | ||
render_start_line = tonumber(scroll_spec) | ||
end | ||
|
||
self:updateScrollbar(render_start_line) | ||
end | ||
|
||
function TextArea:updateScrollbar(scrollbar_current_y) | ||
local lines_count = #self.text_area.wrapped_text.lines | ||
|
||
local render_start_line_y = (math.min( | ||
#self.text_area.wrapped_text.lines - self.text_area.frame_body.height + 1, | ||
math.max(1, scrollbar_current_y) | ||
)) | ||
|
||
self.scrollbar:update( | ||
render_start_line_y, | ||
self.frame_body.height, | ||
lines_count | ||
) | ||
|
||
if (self.frame_body.height >= lines_count) then | ||
render_start_line_y = 1 | ||
end | ||
|
||
self.render_start_line_y = render_start_line_y | ||
self.text_area:setRenderStartLineY(self.render_start_line_y) | ||
end | ||
|
||
function TextArea:renderSubviews(dc) | ||
self.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) | ||
|
||
TextArea.super.renderSubviews(self, dc) | ||
end | ||
|
||
function TextArea:onInput(keys) | ||
if (self.scrollbar.is_dragging) then | ||
return self.scrollbar:onInput(keys) | ||
end | ||
|
||
if keys._MOUSE_L and self:getMousePos() then | ||
self:setFocus(true) | ||
end | ||
|
||
return TextArea.super.onInput(self, keys) | ||
end | ||
|
||
return TextArea |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
HistoryStore = defclass(HistoryStore) | ||
|
||
local HISTORY_ENTRY = { | ||
TEXT_BLOCK = 1, | ||
WHITESPACE_BLOCK = 2, | ||
BACKSPACE = 2, | ||
DELETE = 3, | ||
OTHER = 4 | ||
} | ||
|
||
HistoryStore.ATTRS{ | ||
history_size = 25, | ||
} | ||
|
||
function HistoryStore:init() | ||
self.past = {} | ||
self.future = {} | ||
end | ||
|
||
function HistoryStore:store(history_entry_type, text, cursor) | ||
local last_entry = self.past[#self.past] | ||
|
||
if not last_entry or history_entry_type == HISTORY_ENTRY.OTHER or | ||
last_entry.entry_type ~= history_entry_type then | ||
table.insert(self.past, { | ||
entry_type=history_entry_type, | ||
text=text, | ||
cursor=cursor | ||
}) | ||
end | ||
|
||
self.future = {} | ||
|
||
if #self.past > self.history_size then | ||
table.remove(self.past, 1) | ||
end | ||
end | ||
|
||
function HistoryStore:undo(curr_text, curr_cursor) | ||
if #self.past == 0 then | ||
return nil | ||
end | ||
|
||
local history_entry = table.remove(self.past, #self.past) | ||
|
||
table.insert(self.future, { | ||
entry_type=HISTORY_ENTRY.OTHER, | ||
text=curr_text, | ||
cursor=curr_cursor | ||
}) | ||
|
||
if #self.future > self.history_size then | ||
table.remove(self.future, 1) | ||
end | ||
|
||
return history_entry | ||
end | ||
|
||
function HistoryStore:redo(curr_text, curr_cursor) | ||
if #self.future == 0 then | ||
return true | ||
end | ||
|
||
local history_entry = table.remove(self.future, #self.future) | ||
|
||
table.insert(self.past, { | ||
entry_type=HISTORY_ENTRY.OTHER, | ||
text=curr_text, | ||
cursor=curr_cursor | ||
}) | ||
|
||
if #self.past > self.history_size then | ||
table.remove(self.past, 1) | ||
end | ||
|
||
return history_entry | ||
end | ||
|
||
function HistoryStore:clear() | ||
self.past = {} | ||
self.future = {} | ||
end | ||
|
||
HistoryStore.HISTORY_ENTRY = HISTORY_ENTRY | ||
|
||
return HistoryStore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it makes sense to switch this to
text
instead ofinit_text
? It feels liketext
might align more closely with the API of other widgets likeEditField
,Label
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am afraid it will consfuse users and they will treat it as writeable property - where it is not. they need to to use
:setText(...)