2012-10-15 10:03:18 -06:00
|
|
|
-- Simple widgets for screens
|
|
|
|
|
|
|
|
local _ENV = mkmodule('gui.widgets')
|
|
|
|
|
|
|
|
local gui = require('gui')
|
|
|
|
local utils = require('utils')
|
|
|
|
|
|
|
|
local dscreen = dfhack.screen
|
2022-12-02 16:36:45 -07:00
|
|
|
local getval = utils.getval
|
2012-10-15 10:03:18 -06:00
|
|
|
|
2012-10-17 01:49:11 -06:00
|
|
|
local function show_view(view,vis)
|
2012-10-16 04:18:35 -06:00
|
|
|
if view then
|
|
|
|
view.visible = vis
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-17 00:41:50 -06:00
|
|
|
local function map_opttab(tab,idx)
|
|
|
|
if tab then
|
|
|
|
return tab[idx]
|
|
|
|
else
|
|
|
|
return idx
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-01-30 17:40:15 -07:00
|
|
|
STANDARDSCROLL = {
|
|
|
|
STANDARDSCROLL_UP = -1,
|
|
|
|
STANDARDSCROLL_DOWN = 1,
|
|
|
|
STANDARDSCROLL_PAGEUP = '-page',
|
|
|
|
STANDARDSCROLL_PAGEDOWN = '+page',
|
|
|
|
}
|
|
|
|
|
|
|
|
SECONDSCROLL = {
|
|
|
|
SECONDSCROLL_UP = -1,
|
|
|
|
SECONDSCROLL_DOWN = 1,
|
|
|
|
SECONDSCROLL_PAGEUP = '-page',
|
|
|
|
SECONDSCROLL_PAGEDOWN = '+page',
|
|
|
|
}
|
|
|
|
|
2012-10-15 10:03:18 -06:00
|
|
|
------------
|
|
|
|
-- Widget --
|
|
|
|
------------
|
|
|
|
|
|
|
|
Widget = defclass(Widget, gui.View)
|
|
|
|
|
|
|
|
Widget.ATTRS {
|
|
|
|
frame = DEFAULT_NIL,
|
|
|
|
frame_inset = DEFAULT_NIL,
|
|
|
|
frame_background = DEFAULT_NIL,
|
|
|
|
}
|
|
|
|
|
|
|
|
function Widget:computeFrame(parent_rect)
|
|
|
|
local sw, sh = parent_rect.width, parent_rect.height
|
|
|
|
return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset)
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function Widget:onRenderFrame(dc, rect)
|
2012-10-15 10:03:18 -06:00
|
|
|
if self.frame_background then
|
2012-10-16 04:18:35 -06:00
|
|
|
dc:fill(rect, self.frame_background)
|
2012-10-15 10:03:18 -06:00
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
-----------
|
|
|
|
-- Panel --
|
|
|
|
-----------
|
|
|
|
|
|
|
|
Panel = defclass(Panel, Widget)
|
2012-10-15 10:03:18 -06:00
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
Panel.ATTRS {
|
2022-11-06 17:42:01 -07:00
|
|
|
frame_style = DEFAULT_NIL, -- as in gui.FramedScreen
|
|
|
|
frame_title = DEFAULT_NIL, -- as in gui.FramedScreen
|
2012-10-16 04:18:35 -06:00
|
|
|
on_render = DEFAULT_NIL,
|
2012-11-30 08:10:17 -07:00
|
|
|
on_layout = DEFAULT_NIL,
|
2022-04-11 19:25:00 -06:00
|
|
|
autoarrange_subviews = false, -- whether to automatically lay out subviews
|
|
|
|
autoarrange_gap = 0, -- how many blank lines to insert between widgets
|
2012-10-16 04:18:35 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
function Panel:init(args)
|
|
|
|
self:addviews(args.subviews)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Panel:onRenderBody(dc)
|
|
|
|
if self.on_render then self.on_render(dc) end
|
|
|
|
end
|
|
|
|
|
2022-11-06 17:42:01 -07:00
|
|
|
function Panel:computeFrame(parent_rect)
|
|
|
|
local sw, sh = parent_rect.width, parent_rect.height
|
|
|
|
return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset,
|
|
|
|
self.frame_style and 1 or 0)
|
|
|
|
end
|
|
|
|
|
2012-11-30 08:10:17 -07:00
|
|
|
function Panel:postComputeFrame(body)
|
|
|
|
if self.on_layout then self.on_layout(body) end
|
|
|
|
end
|
|
|
|
|
2022-04-11 19:25:00 -06:00
|
|
|
-- if self.autoarrange_subviews is true, lay out visible subviews vertically,
|
|
|
|
-- adding gaps between widgets according to self.autoarrange_gap.
|
|
|
|
function Panel:postUpdateLayout()
|
|
|
|
if not self.autoarrange_subviews then return end
|
|
|
|
|
|
|
|
local gap = self.autoarrange_gap
|
|
|
|
local y = 0
|
|
|
|
for _,subview in ipairs(self.subviews) do
|
2022-04-17 21:07:47 -06:00
|
|
|
if not subview.frame then goto continue end
|
2022-04-11 19:25:00 -06:00
|
|
|
subview.frame.t = y
|
2022-12-02 16:36:45 -07:00
|
|
|
if getval(subview.visible) then
|
2022-04-17 21:07:47 -06:00
|
|
|
y = y + (subview.frame.h or 0) + gap
|
2022-04-11 19:25:00 -06:00
|
|
|
end
|
2022-04-17 21:07:47 -06:00
|
|
|
::continue::
|
2022-04-11 19:25:00 -06:00
|
|
|
end
|
|
|
|
self.frame_rect.height = y
|
|
|
|
|
|
|
|
-- let widgets adjust to their new positions
|
|
|
|
self:updateSubviewLayout()
|
|
|
|
end
|
|
|
|
|
2022-11-06 17:42:01 -07:00
|
|
|
function Panel:onRenderFrame(dc, rect)
|
|
|
|
Panel.super.onRenderFrame(self, dc, rect)
|
|
|
|
if not self.frame_style then return end
|
|
|
|
local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2
|
|
|
|
gui.paint_frame(x1, y1, x2, y2, self.frame_style, self.frame_title)
|
|
|
|
end
|
|
|
|
|
2022-04-11 19:25:00 -06:00
|
|
|
-------------------
|
|
|
|
-- ResizingPanel --
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
ResizingPanel = defclass(ResizingPanel, Panel)
|
|
|
|
|
|
|
|
-- adjust our frame dimensions according to positions and sizes of our subviews
|
|
|
|
function ResizingPanel:postUpdateLayout(frame_body)
|
|
|
|
local w, h = 0, 0
|
2022-07-20 16:36:17 -06:00
|
|
|
for _,s in ipairs(self.subviews) do
|
2022-12-02 16:36:45 -07:00
|
|
|
if getval(s.visible) then
|
2022-07-20 16:36:17 -06:00
|
|
|
w = math.max(w, (s.frame and s.frame.l or 0) +
|
|
|
|
(s.frame and s.frame.w or frame_body.width))
|
|
|
|
h = math.max(h, (s.frame and s.frame.t or 0) +
|
|
|
|
(s.frame and s.frame.h or frame_body.height))
|
2022-04-11 19:25:00 -06:00
|
|
|
end
|
|
|
|
end
|
2022-11-28 16:12:22 -07:00
|
|
|
local l,t,r,b = gui.parse_inset(self.frame_inset)
|
|
|
|
w = w + l + r
|
|
|
|
h = h + t + b
|
2022-11-06 17:42:01 -07:00
|
|
|
if self.frame_style then
|
|
|
|
w = w + 2
|
|
|
|
h = h + 2
|
|
|
|
end
|
2022-04-17 21:07:47 -06:00
|
|
|
if not self.frame then self.frame = {} end
|
2022-11-06 17:42:01 -07:00
|
|
|
local oldw, oldh = self.frame.w, self.frame.h
|
2022-04-11 19:25:00 -06:00
|
|
|
self.frame.w, self.frame.h = w, h
|
2022-11-06 17:42:01 -07:00
|
|
|
if not self._updateLayoutGuard and (oldw ~= w or oldh ~= h) then
|
|
|
|
self._updateLayoutGuard = true -- protect against infinite loops
|
|
|
|
self:updateLayout() -- our frame has changed, we need to fully refresh
|
|
|
|
end
|
|
|
|
self._updateLayoutGuard = nil
|
2022-04-11 19:25:00 -06:00
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
-----------
|
|
|
|
-- Pages --
|
|
|
|
-----------
|
|
|
|
|
|
|
|
Pages = defclass(Pages, Panel)
|
|
|
|
|
|
|
|
function Pages:init(args)
|
|
|
|
for _,v in ipairs(self.subviews) do
|
2012-10-17 01:49:11 -06:00
|
|
|
v.visible = false
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
self:setSelected(args.selected or 1)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pages:setSelected(idx)
|
|
|
|
if type(idx) ~= 'number' then
|
|
|
|
local key = idx
|
|
|
|
if type(idx) == 'string' then
|
|
|
|
key = self.subviews[key]
|
|
|
|
end
|
|
|
|
idx = utils.linear_index(self.subviews, key)
|
|
|
|
if not idx then
|
|
|
|
error('Unknown page: '..key)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-17 01:49:11 -06:00
|
|
|
show_view(self.subviews[self.selected], false)
|
2012-10-16 04:18:35 -06:00
|
|
|
self.selected = math.min(math.max(1, idx), #self.subviews)
|
2012-10-17 01:49:11 -06:00
|
|
|
show_view(self.subviews[self.selected], true)
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function Pages:getSelected()
|
|
|
|
return self.selected, self.subviews[self.selected]
|
2012-10-15 10:03:18 -06:00
|
|
|
end
|
|
|
|
|
2017-06-23 10:46:46 -06:00
|
|
|
function Pages:getSelectedPage()
|
|
|
|
return self.subviews[self.selected]
|
|
|
|
end
|
|
|
|
|
2012-10-15 10:03:18 -06:00
|
|
|
----------------
|
|
|
|
-- Edit field --
|
|
|
|
----------------
|
|
|
|
|
|
|
|
EditField = defclass(EditField, Widget)
|
|
|
|
|
|
|
|
EditField.ATTRS{
|
2022-05-18 17:35:06 -06:00
|
|
|
label_text = DEFAULT_NIL,
|
2012-10-15 10:03:18 -06:00
|
|
|
text = '',
|
|
|
|
text_pen = DEFAULT_NIL,
|
|
|
|
on_char = DEFAULT_NIL,
|
|
|
|
on_change = DEFAULT_NIL,
|
2012-10-16 04:18:35 -06:00
|
|
|
on_submit = DEFAULT_NIL,
|
2022-07-15 14:46:01 -06:00
|
|
|
on_submit2 = DEFAULT_NIL,
|
2017-06-27 19:10:14 -06:00
|
|
|
key = DEFAULT_NIL,
|
2022-05-18 17:35:06 -06:00
|
|
|
key_sep = DEFAULT_NIL,
|
2022-06-01 22:48:21 -06:00
|
|
|
modal = false,
|
2022-08-29 12:40:56 -06:00
|
|
|
ignore_keys = DEFAULT_NIL,
|
2012-10-15 10:03:18 -06:00
|
|
|
}
|
|
|
|
|
2022-07-17 17:04:36 -06:00
|
|
|
function EditField:preinit(init_table)
|
2022-07-20 16:36:17 -06:00
|
|
|
init_table.frame = init_table.frame or {}
|
|
|
|
init_table.frame.h = init_table.frame.h or 1
|
2022-07-17 17:04:36 -06:00
|
|
|
end
|
|
|
|
|
2022-05-18 17:35:06 -06:00
|
|
|
function EditField:init()
|
2022-06-01 22:48:21 -06:00
|
|
|
local function on_activate()
|
|
|
|
self.saved_text = self.text
|
|
|
|
self:setFocus(true)
|
|
|
|
end
|
|
|
|
|
2022-07-17 16:43:58 -06:00
|
|
|
self.start_pos = 1
|
2022-07-20 16:36:17 -06:00
|
|
|
self.cursor = #self.text + 1
|
2022-07-15 23:22:51 -06:00
|
|
|
|
2022-05-18 17:35:06 -06:00
|
|
|
self:addviews{HotkeyLabel{frame={t=0,l=0},
|
|
|
|
key=self.key,
|
|
|
|
key_sep=self.key_sep,
|
2022-06-01 22:48:21 -06:00
|
|
|
label=self.label_text,
|
|
|
|
on_activate=self.key and on_activate or nil}}
|
|
|
|
end
|
|
|
|
|
|
|
|
function EditField:getPreferredFocusState()
|
|
|
|
return not self.key
|
2022-05-18 17:35:06 -06:00
|
|
|
end
|
|
|
|
|
2022-07-15 23:22:51 -06:00
|
|
|
function EditField:setCursor(cursor)
|
|
|
|
if not cursor or cursor > #self.text then
|
|
|
|
self.cursor = #self.text + 1
|
|
|
|
return
|
|
|
|
end
|
|
|
|
self.cursor = math.max(1, cursor)
|
|
|
|
end
|
|
|
|
|
2022-07-20 16:40:49 -06:00
|
|
|
function EditField:setText(text, cursor)
|
|
|
|
self.text = text
|
2022-07-15 23:22:51 -06:00
|
|
|
self:setCursor(cursor)
|
2022-07-20 16:40:49 -06:00
|
|
|
end
|
|
|
|
|
2022-05-18 17:35:06 -06:00
|
|
|
function EditField:postUpdateLayout()
|
|
|
|
self.text_offset = self.subviews[1]:getTextWidth()
|
|
|
|
end
|
|
|
|
|
2012-10-15 10:03:18 -06:00
|
|
|
function EditField:onRenderBody(dc)
|
|
|
|
dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0)
|
|
|
|
|
2022-07-15 23:22:51 -06:00
|
|
|
local cursor_char = '_'
|
2022-12-02 16:36:45 -07:00
|
|
|
if not getval(self.active) or not self.focus or gui.blink_visible(300) then
|
2022-07-15 23:22:51 -06:00
|
|
|
cursor_char = (self.cursor > #self.text) and ' ' or
|
|
|
|
self.text:sub(self.cursor, self.cursor)
|
2012-10-15 10:03:18 -06:00
|
|
|
end
|
2022-07-15 23:22:51 -06:00
|
|
|
local txt = self.text:sub(1, self.cursor - 1) .. cursor_char ..
|
|
|
|
self.text:sub(self.cursor + 1)
|
2022-05-18 17:35:06 -06:00
|
|
|
local max_width = dc.width - self.text_offset
|
2022-07-17 16:43:58 -06:00
|
|
|
self.start_pos = 1
|
2017-06-27 19:10:14 -06:00
|
|
|
if #txt > max_width then
|
2022-07-15 23:22:51 -06:00
|
|
|
-- 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
|
2022-07-17 16:43:58 -06:00
|
|
|
start_pos = #txt - (max_width - 1)
|
2022-07-15 23:22:51 -06:00
|
|
|
end
|
|
|
|
if self.cursor - half_width <= 1 then
|
|
|
|
end_pos = max_width + 1
|
|
|
|
end
|
2022-07-17 16:43:58 -06:00
|
|
|
self.start_pos = start_pos > 1 and start_pos - 1 or start_pos
|
2022-07-15 23:22:51 -06:00
|
|
|
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))
|
2012-10-15 10:03:18 -06:00
|
|
|
end
|
2022-05-18 17:35:06 -06:00
|
|
|
dc:advance(self.text_offset):string(txt)
|
2022-09-16 16:24:33 -06:00
|
|
|
dc:string((' '):rep(dc.clip_x2 - dc.x))
|
2012-10-15 10:03:18 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function EditField:onInput(keys)
|
2022-06-01 22:48:21 -06:00
|
|
|
if not self.focus then
|
|
|
|
-- only react to our hotkey
|
|
|
|
return self:inputToSubviews(keys)
|
|
|
|
end
|
|
|
|
|
2022-08-29 12:40:56 -06:00
|
|
|
if self.ignore_keys then
|
|
|
|
for _,ignore_key in ipairs(self.ignore_keys) do
|
|
|
|
if keys[ignore_key] then return false end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-06-01 22:48:21 -06:00
|
|
|
if self.key and keys.LEAVESCREEN then
|
|
|
|
local old = self.text
|
2022-07-16 23:03:39 -06:00
|
|
|
self:setText(self.saved_text)
|
2022-06-01 22:48:21 -06:00
|
|
|
if self.on_change and old ~= self.saved_text then
|
|
|
|
self.on_change(self.text, old)
|
|
|
|
end
|
|
|
|
self:setFocus(false)
|
2012-10-16 04:18:35 -06:00
|
|
|
return true
|
2022-06-01 22:48:21 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
if keys.SELECT then
|
|
|
|
if self.key then
|
|
|
|
self:setFocus(false)
|
|
|
|
end
|
|
|
|
if self.on_submit then
|
|
|
|
self.on_submit(self.text)
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
return not not self.key
|
2022-07-15 23:22:51 -06:00
|
|
|
elseif keys.SEC_SELECT then
|
2022-07-15 14:46:01 -06:00
|
|
|
if self.key then
|
|
|
|
self:setFocus(false)
|
|
|
|
end
|
|
|
|
if self.on_submit2 then
|
|
|
|
self.on_submit2(self.text)
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
return not not self.key
|
2022-07-16 23:03:39 -06:00
|
|
|
elseif keys._MOUSE_L then
|
|
|
|
local mouse_x, mouse_y = self:getMousePos()
|
|
|
|
if mouse_x then
|
2022-11-21 18:51:04 -07:00
|
|
|
self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0))
|
2022-07-16 23:03:39 -06:00
|
|
|
return true
|
|
|
|
end
|
2022-08-19 23:40:53 -06:00
|
|
|
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:setText(old:sub(1,self.cursor-1)..cv..old:sub(self.cursor),
|
|
|
|
self.cursor + 1)
|
2022-10-21 13:43:53 -06:00
|
|
|
elseif self.on_char then
|
|
|
|
return self.modal
|
2022-08-19 23:40:53 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
if self.on_change and self.text ~= old then
|
|
|
|
self.on_change(self.text, old)
|
|
|
|
end
|
|
|
|
return true
|
2022-07-15 23:22:51 -06:00
|
|
|
elseif keys.CURSOR_LEFT then
|
2022-07-16 23:03:39 -06:00
|
|
|
self:setCursor(self.cursor - 1)
|
2022-07-15 23:22:51 -06:00
|
|
|
return true
|
2022-08-08 12:44:33 -06:00
|
|
|
elseif keys.A_MOVE_W_DOWN then -- Ctrl-Left (end of prev word)
|
|
|
|
local _, prev_word_end = self.text:sub(1, self.cursor-1):
|
|
|
|
find('.*[%w_%-][^%w_%-]')
|
|
|
|
self:setCursor(prev_word_end or 1)
|
2022-07-15 23:22:51 -06:00
|
|
|
return true
|
|
|
|
elseif keys.A_CARE_MOVE_W then -- Alt-Left (home)
|
2022-07-16 23:03:39 -06:00
|
|
|
self:setCursor(1)
|
2022-07-15 23:22:51 -06:00
|
|
|
return true
|
|
|
|
elseif keys.CURSOR_RIGHT then
|
2022-07-16 23:03:39 -06:00
|
|
|
self:setCursor(self.cursor + 1)
|
2022-07-15 23:22:51 -06:00
|
|
|
return true
|
2022-08-08 12:44:33 -06:00
|
|
|
elseif keys.A_MOVE_E_DOWN then -- Ctrl-Right (beginning of next word)
|
|
|
|
local _,next_word_start = self.text:find('[^%w_%-][%w_%-]', self.cursor)
|
|
|
|
self:setCursor(next_word_start)
|
2022-07-15 23:22:51 -06:00
|
|
|
return true
|
|
|
|
elseif keys.A_CARE_MOVE_E then -- Alt-Right (end)
|
2022-07-16 23:03:39 -06:00
|
|
|
self:setCursor()
|
2022-07-15 23:22:51 -06:00
|
|
|
return true
|
2012-10-15 10:03:18 -06:00
|
|
|
end
|
2022-06-01 22:48:21 -06:00
|
|
|
|
|
|
|
-- if we're modal, then unconditionally eat all the input
|
|
|
|
return self.modal
|
2012-10-15 10:03:18 -06:00
|
|
|
end
|
|
|
|
|
2022-10-06 12:13:16 -06:00
|
|
|
---------------
|
|
|
|
-- Scrollbar --
|
|
|
|
---------------
|
|
|
|
|
2022-10-07 16:40:05 -06:00
|
|
|
SCROLL_INITIAL_DELAY_MS = 300
|
|
|
|
SCROLL_DELAY_MS = 20
|
|
|
|
|
2022-10-06 12:13:16 -06:00
|
|
|
Scrollbar = defclass(Scrollbar, Widget)
|
|
|
|
|
|
|
|
Scrollbar.ATTRS{
|
|
|
|
fg = COLOR_LIGHTGREEN,
|
|
|
|
bg = COLOR_CYAN,
|
|
|
|
on_scroll = DEFAULT_NIL,
|
|
|
|
}
|
|
|
|
|
|
|
|
function Scrollbar:preinit(init_table)
|
|
|
|
init_table.frame = init_table.frame or {}
|
|
|
|
init_table.frame.w = init_table.frame.w or 1
|
|
|
|
end
|
|
|
|
|
|
|
|
function Scrollbar:init()
|
2022-10-07 16:40:05 -06:00
|
|
|
self.last_scroll_ms = 0
|
|
|
|
self.is_first_click = false
|
|
|
|
self.scroll_spec = nil
|
2022-10-07 17:27:19 -06:00
|
|
|
self.is_dragging = false -- index of the scrollbar tile that we're dragging
|
2022-10-06 12:13:16 -06:00
|
|
|
self:update(1, 1, 1)
|
|
|
|
end
|
|
|
|
|
2022-10-21 13:17:59 -06:00
|
|
|
local function scrollbar_get_max_pos_and_height(scrollbar)
|
|
|
|
local frame_body = scrollbar.frame_body
|
|
|
|
local scrollbar_body_height = (frame_body and frame_body.height or 3) - 2
|
|
|
|
|
|
|
|
local height = math.max(1, math.floor(
|
|
|
|
(math.min(scrollbar.elems_per_page, scrollbar.num_elems) * scrollbar_body_height) /
|
|
|
|
scrollbar.num_elems))
|
|
|
|
|
|
|
|
return scrollbar_body_height - height, height
|
|
|
|
end
|
|
|
|
|
2022-10-06 12:13:16 -06:00
|
|
|
-- calculate and cache the number of tiles of empty space above the top of the
|
|
|
|
-- scrollbar and the number of tiles the scrollbar should occupy to represent
|
|
|
|
-- the percentage of text that is on the screen.
|
|
|
|
-- if elems_per_page or num_elems are not specified, the last values passed to
|
|
|
|
-- Scrollbar:update() are used.
|
|
|
|
function Scrollbar:update(top_elem, elems_per_page, num_elems)
|
|
|
|
if not top_elem then error('must specify index of new top element') end
|
|
|
|
elems_per_page = elems_per_page or self.elems_per_page
|
|
|
|
num_elems = num_elems or self.num_elems
|
2022-10-21 13:17:59 -06:00
|
|
|
self.top_elem = top_elem
|
|
|
|
self.elems_per_page, self.num_elems = elems_per_page, num_elems
|
2022-10-06 12:13:16 -06:00
|
|
|
|
2022-10-21 13:17:59 -06:00
|
|
|
local max_pos, height = scrollbar_get_max_pos_and_height(self)
|
2022-10-07 14:14:52 -06:00
|
|
|
local pos = (num_elems == elems_per_page) and 0 or
|
|
|
|
math.ceil(((top_elem-1) * max_pos) /
|
|
|
|
(num_elems - elems_per_page))
|
2022-10-06 12:13:16 -06:00
|
|
|
|
|
|
|
self.bar_offset, self.bar_height = pos, height
|
|
|
|
end
|
|
|
|
|
2022-10-07 17:27:19 -06:00
|
|
|
local function scrollbar_do_drag(scrollbar)
|
2022-10-21 13:17:59 -06:00
|
|
|
local _,y = scrollbar.frame_body:localXY(dfhack.screen.getMousePos())
|
|
|
|
local cur_pos = y - scrollbar.is_dragging
|
|
|
|
local max_top = scrollbar.num_elems - scrollbar.elems_per_page + 1
|
|
|
|
local max_pos = scrollbar_get_max_pos_and_height(scrollbar)
|
|
|
|
local new_top_elem = math.floor(cur_pos * max_top / max_pos) + 1
|
|
|
|
new_top_elem = math.max(1, math.min(new_top_elem, max_top))
|
|
|
|
if new_top_elem ~= scrollbar.top_elem then
|
|
|
|
scrollbar.on_scroll(new_top_elem)
|
2022-10-07 17:27:19 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-21 13:17:59 -06:00
|
|
|
local function scrollbar_is_visible(scrollbar)
|
|
|
|
return scrollbar.elems_per_page < scrollbar.num_elems
|
|
|
|
end
|
|
|
|
|
2022-10-06 12:13:16 -06:00
|
|
|
local UP_ARROW_CHAR = string.char(24)
|
|
|
|
local DOWN_ARROW_CHAR = string.char(25)
|
|
|
|
local NO_ARROW_CHAR = string.char(32)
|
|
|
|
local BAR_CHAR = string.char(7)
|
|
|
|
local BAR_BG_CHAR = string.char(179)
|
|
|
|
|
|
|
|
function Scrollbar:onRenderBody(dc)
|
|
|
|
-- don't draw if all elements are visible
|
2022-11-07 17:13:45 -07:00
|
|
|
if not scrollbar_is_visible(self) then
|
|
|
|
return
|
|
|
|
end
|
2022-10-06 12:13:16 -06:00
|
|
|
-- render up arrow if we're not at the top
|
|
|
|
dc:seek(0, 0):char(
|
|
|
|
self.top_elem == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR, self.fg, self.bg)
|
|
|
|
-- render scrollbar body
|
|
|
|
local starty = self.bar_offset + 1
|
|
|
|
local endy = self.bar_offset + self.bar_height
|
|
|
|
for y=1,dc.height-2 do
|
|
|
|
dc:seek(0, y)
|
|
|
|
if y >= starty and y <= endy then
|
|
|
|
dc:char(BAR_CHAR, self.fg)
|
|
|
|
else
|
|
|
|
dc:char(BAR_BG_CHAR, self.bg)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- render down arrow if we're not at the bottom
|
|
|
|
local last_visible_el = self.top_elem + self.elems_per_page - 1
|
|
|
|
dc:seek(0, dc.height-1):char(
|
|
|
|
last_visible_el >= self.num_elems and NO_ARROW_CHAR or DOWN_ARROW_CHAR,
|
|
|
|
self.fg, self.bg)
|
2022-10-07 17:27:19 -06:00
|
|
|
if not self.on_scroll then return end
|
|
|
|
-- manage state for dragging and continuous scrolling
|
|
|
|
if self.is_dragging then
|
|
|
|
scrollbar_do_drag(self)
|
|
|
|
end
|
2022-11-21 18:36:46 -07:00
|
|
|
if df.global.enabler.mouse_lbut == 0 then
|
2022-10-07 16:40:05 -06:00
|
|
|
self.last_scroll_ms = 0
|
2022-10-07 17:27:19 -06:00
|
|
|
self.is_dragging = false
|
2022-10-07 16:40:05 -06:00
|
|
|
self.scroll_spec = nil
|
|
|
|
return
|
|
|
|
end
|
2022-10-07 17:27:19 -06:00
|
|
|
if self.last_scroll_ms == 0 then return end
|
2022-10-07 16:40:05 -06:00
|
|
|
local now = dfhack.getTickCount()
|
|
|
|
local delay = self.is_first_click and
|
|
|
|
SCROLL_INITIAL_DELAY_MS or SCROLL_DELAY_MS
|
|
|
|
if now - self.last_scroll_ms >= delay then
|
|
|
|
self.is_first_click = false
|
|
|
|
self.on_scroll(self.scroll_spec)
|
|
|
|
self.last_scroll_ms = now
|
|
|
|
end
|
2022-10-06 12:13:16 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function Scrollbar:onInput(keys)
|
2022-10-21 13:17:59 -06:00
|
|
|
if not keys._MOUSE_L_DOWN or not self.on_scroll
|
|
|
|
or not scrollbar_is_visible(self) then
|
|
|
|
return false
|
|
|
|
end
|
2022-10-06 12:13:16 -06:00
|
|
|
local _,y = self:getMousePos()
|
|
|
|
if not y then return false end
|
2022-10-07 16:40:05 -06:00
|
|
|
local scroll_spec = nil
|
|
|
|
if y == 0 then scroll_spec = 'up_small'
|
|
|
|
elseif y == self.frame_body.height - 1 then scroll_spec = 'down_small'
|
|
|
|
elseif y <= self.bar_offset then scroll_spec = 'up_large'
|
|
|
|
elseif y > self.bar_offset + self.bar_height then scroll_spec = 'down_large'
|
2022-10-07 17:27:19 -06:00
|
|
|
else
|
|
|
|
self.is_dragging = y - self.bar_offset
|
|
|
|
return true
|
2022-10-06 12:13:16 -06:00
|
|
|
end
|
2022-10-07 16:40:05 -06:00
|
|
|
self.scroll_spec = scroll_spec
|
2022-10-07 17:27:19 -06:00
|
|
|
self.on_scroll(scroll_spec)
|
2022-10-07 16:40:05 -06:00
|
|
|
-- reset continuous scroll state
|
|
|
|
self.is_first_click = true
|
|
|
|
self.last_scroll_ms = dfhack.getTickCount()
|
2022-10-06 12:13:16 -06:00
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
-----------
|
|
|
|
-- Label --
|
|
|
|
-----------
|
|
|
|
|
|
|
|
function parse_label_text(obj)
|
|
|
|
local text = obj.text or {}
|
|
|
|
if type(text) ~= 'table' then
|
|
|
|
text = { text }
|
|
|
|
end
|
|
|
|
local curline = nil
|
|
|
|
local out = { }
|
|
|
|
local active = nil
|
|
|
|
local idtab = nil
|
|
|
|
for _,v in ipairs(text) do
|
|
|
|
local vv
|
|
|
|
if type(v) == 'string' then
|
2021-07-03 00:30:59 -06:00
|
|
|
vv = v:split(NEWLINE)
|
2012-10-16 04:18:35 -06:00
|
|
|
else
|
|
|
|
vv = { v }
|
|
|
|
end
|
|
|
|
|
|
|
|
for i = 1,#vv do
|
|
|
|
local cv = vv[i]
|
|
|
|
if i > 1 then
|
|
|
|
if not curline then
|
|
|
|
table.insert(out, {})
|
|
|
|
end
|
|
|
|
curline = nil
|
|
|
|
end
|
|
|
|
if cv ~= '' then
|
|
|
|
if not curline then
|
|
|
|
curline = {}
|
|
|
|
table.insert(out, curline)
|
|
|
|
end
|
|
|
|
|
|
|
|
if type(cv) == 'string' then
|
|
|
|
table.insert(curline, { text = cv })
|
|
|
|
else
|
|
|
|
table.insert(curline, cv)
|
|
|
|
|
|
|
|
if cv.on_activate then
|
|
|
|
active = active or {}
|
|
|
|
table.insert(active, cv)
|
|
|
|
end
|
|
|
|
|
|
|
|
if cv.id then
|
|
|
|
idtab = idtab or {}
|
|
|
|
idtab[cv.id] = cv
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
obj.text_lines = out
|
|
|
|
obj.text_active = active
|
|
|
|
obj.text_ids = idtab
|
|
|
|
end
|
|
|
|
|
2012-10-20 11:57:36 -06:00
|
|
|
local function is_disabled(token)
|
|
|
|
return (token.disabled ~= nil and getval(token.disabled)) or
|
|
|
|
(token.enabled ~= nil and not getval(token.enabled))
|
|
|
|
end
|
|
|
|
|
2012-10-24 09:25:06 -06:00
|
|
|
function render_text(obj,dc,x0,y0,pen,dpen,disabled)
|
2012-10-16 04:18:35 -06:00
|
|
|
local width = 0
|
2021-01-13 00:27:14 -07:00
|
|
|
for iline = dc and obj.start_line_num or 1, #obj.text_lines do
|
|
|
|
local x, line = 0, obj.text_lines[iline]
|
2012-10-16 04:18:35 -06:00
|
|
|
if dc then
|
2020-11-04 19:06:50 -07:00
|
|
|
local offset = (obj.start_line_num or 1) - 1
|
2021-01-13 23:02:22 -07:00
|
|
|
local y = y0 + iline - offset - 1
|
|
|
|
-- skip text outside of the containing frame
|
|
|
|
if y > dc.height - 1 then break end
|
|
|
|
dc:seek(x+x0, y)
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
for _,token in ipairs(line) do
|
|
|
|
token.line = iline
|
|
|
|
token.x1 = x
|
|
|
|
|
|
|
|
if token.gap then
|
|
|
|
x = x + token.gap
|
|
|
|
if dc then
|
|
|
|
dc:advance(token.gap)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-16 23:41:48 -06:00
|
|
|
if token.tile then
|
|
|
|
x = x + 1
|
|
|
|
if dc then
|
|
|
|
dc:char(nil, token.tile)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
if token.text or token.key then
|
2012-11-30 08:10:17 -07:00
|
|
|
local text = ''..(getval(token.text) or '')
|
2017-05-28 21:11:37 -06:00
|
|
|
local keypen = dfhack.pen.parse(token.key_pen or COLOR_LIGHTGREEN)
|
2012-10-16 04:18:35 -06:00
|
|
|
|
|
|
|
if dc then
|
2012-10-25 03:20:41 -06:00
|
|
|
local tpen = getval(token.pen)
|
2012-10-24 09:25:06 -06:00
|
|
|
if disabled or is_disabled(token) then
|
2012-10-25 03:20:41 -06:00
|
|
|
dc:pen(getval(token.dpen) or tpen or dpen)
|
2017-05-28 21:11:37 -06:00
|
|
|
if keypen.fg ~= COLOR_BLACK then
|
|
|
|
keypen.bold = false
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
else
|
2012-10-25 03:20:41 -06:00
|
|
|
dc:pen(tpen or pen)
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-11-30 08:10:17 -07:00
|
|
|
local width = getval(token.width)
|
|
|
|
local padstr
|
|
|
|
if width then
|
|
|
|
x = x + width
|
|
|
|
if #text > width then
|
|
|
|
text = string.sub(text,1,width)
|
|
|
|
else
|
|
|
|
if token.pad_char then
|
|
|
|
padstr = string.rep(token.pad_char,width-#text)
|
|
|
|
end
|
|
|
|
if dc and token.rjustify then
|
|
|
|
if padstr then dc:string(padstr) else dc:advance(width-#text) end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
x = x + #text
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
|
|
|
|
if token.key then
|
|
|
|
local keystr = gui.getKeyDisplay(token.key)
|
|
|
|
local sep = token.key_sep or ''
|
|
|
|
|
2012-10-16 08:33:00 -06:00
|
|
|
x = x + #keystr
|
|
|
|
|
2022-05-18 17:35:06 -06:00
|
|
|
if sep:startswith('()') then
|
2012-10-16 04:18:35 -06:00
|
|
|
if dc then
|
|
|
|
dc:string(text)
|
2022-05-18 17:35:06 -06:00
|
|
|
dc:string(' ('):string(keystr,keypen)
|
|
|
|
dc:string(sep:sub(2))
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
2022-05-18 17:35:06 -06:00
|
|
|
x = x + 1 + #sep
|
2012-10-16 04:18:35 -06:00
|
|
|
else
|
|
|
|
if dc then
|
|
|
|
dc:string(keystr,keypen):string(sep):string(text)
|
|
|
|
end
|
|
|
|
x = x + #sep
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if dc then
|
|
|
|
dc:string(text)
|
|
|
|
end
|
|
|
|
end
|
2012-11-30 08:10:17 -07:00
|
|
|
|
|
|
|
if width and dc and not token.rjustify then
|
|
|
|
if padstr then dc:string(padstr) else dc:advance(width-#text) end
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
token.x2 = x
|
|
|
|
end
|
|
|
|
width = math.max(width, x)
|
|
|
|
end
|
|
|
|
obj.text_width = width
|
|
|
|
end
|
|
|
|
|
|
|
|
function check_text_keys(self, keys)
|
|
|
|
if self.text_active then
|
|
|
|
for _,item in ipairs(self.text_active) do
|
2012-10-20 11:57:36 -06:00
|
|
|
if item.key and keys[item.key] and not is_disabled(item) then
|
2012-10-16 04:18:35 -06:00
|
|
|
item.on_activate()
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
Label = defclass(Label, Widget)
|
|
|
|
|
|
|
|
Label.ATTRS{
|
|
|
|
text_pen = COLOR_WHITE,
|
2016-05-09 19:29:04 -06:00
|
|
|
text_dpen = COLOR_DARKGREY, -- disabled
|
|
|
|
text_hpen = DEFAULT_NIL, -- highlight - default is text_pen with reversed brightness
|
2012-10-24 09:25:06 -06:00
|
|
|
disabled = DEFAULT_NIL,
|
|
|
|
enabled = DEFAULT_NIL,
|
2012-10-16 04:18:35 -06:00
|
|
|
auto_height = true,
|
2012-10-16 08:33:00 -06:00
|
|
|
auto_width = false,
|
2016-05-09 19:29:04 -06:00
|
|
|
on_click = DEFAULT_NIL,
|
|
|
|
on_rclick = DEFAULT_NIL,
|
2020-11-04 19:06:50 -07:00
|
|
|
scroll_keys = STANDARDSCROLL,
|
2012-10-16 04:18:35 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
function Label:init(args)
|
2022-10-07 13:45:43 -06:00
|
|
|
self.scrollbar = Scrollbar{
|
|
|
|
frame={r=0},
|
|
|
|
on_scroll=self:callback('on_scrollbar')}
|
|
|
|
|
|
|
|
self:addviews{self.scrollbar}
|
|
|
|
|
2022-04-17 21:07:47 -06:00
|
|
|
-- use existing saved text if no explicit text was specified. this avoids
|
|
|
|
-- overwriting pre-formatted text that subclasses may have already set
|
|
|
|
self:setText(args.text or self.text)
|
2016-05-09 19:29:04 -06:00
|
|
|
if not self.text_hpen then
|
|
|
|
self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
2022-10-07 13:45:43 -06:00
|
|
|
local function update_label_scrollbar(label)
|
|
|
|
local body_height = label.frame_body and label.frame_body.height or 1
|
|
|
|
label.scrollbar:update(label.start_line_num, body_height,
|
|
|
|
label:getTextHeight())
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function Label:setText(text)
|
2022-07-11 18:23:23 -06:00
|
|
|
self.start_line_num = 1
|
2012-10-16 04:18:35 -06:00
|
|
|
self.text = text
|
|
|
|
parse_label_text(self)
|
|
|
|
|
|
|
|
if self.auto_height then
|
|
|
|
self.frame = self.frame or {}
|
|
|
|
self.frame.h = self:getTextHeight()
|
|
|
|
end
|
|
|
|
|
2022-10-07 13:45:43 -06:00
|
|
|
update_label_scrollbar(self)
|
add scroll icons to Label widget (#2101)
* WIP: add scroll icons to Label widget
It's an opt-out. The icons are rendered in the right-most column of the 1st and last row. They are only rendered when text can actually be scrolled in the corresponding direction.
WIP: Currently, the icons might overlay text characters, there is no mechanism preventing it
* gui.lua: expose the `parse_inset()` function
* refactor Label's scroll icon code
* since `render_scroll_icons` only works with a label, it's now a class function
* `update_scroll_inset` ensures `frame_inset.r` or `.l` is at least 1, according to `show_scroll_icons`
* `show_scroll_icons` has 4 possible values: `false` for no icons, `left` for icons on the first column on the left (also ensuring `frame_inset.l >= 1`), `right` - last column on the right, `DEFAULT_NIL` - same as `right` if text height greater than `frame_body.height`, else same as `false`.
* make `render_scroll_icons` always draw icons
The check now happens in `onRenderFrame`
* draw frame's background
calling `Label.super.onRenderFrame(self, dc, rect)` makes frame's background invisible for some reason
* remove trailing spaces
* fix scroll icons placed far above/below text
With `Label.frame_inset = 1` the text could be vertically centered with plenty of space below and above,
but not all rendered. Before this change, the scroll icons would be at the very top and bottom of the frame
instead of near the first and last rendered text line.
* always `update_scroll_inset` to react to resized window
* draw scroll icons next to text
* update `Lua API.rst` with new `Label` parameters
* move comment separator up
This way every scroll related parameter is in one group
* list default values for new parameters in docs
* add missing description of `Label.scroll_keys`
2022-04-29 07:55:08 -06:00
|
|
|
end
|
|
|
|
|
2012-10-16 08:33:00 -06:00
|
|
|
function Label:preUpdateLayout()
|
|
|
|
if self.auto_width then
|
|
|
|
self.frame = self.frame or {}
|
|
|
|
self.frame.w = self:getTextWidth()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-07 13:45:43 -06:00
|
|
|
function Label:postUpdateLayout()
|
|
|
|
update_label_scrollbar(self)
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function Label:itemById(id)
|
|
|
|
if self.text_ids then
|
|
|
|
return self.text_ids[id]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function Label:getTextHeight()
|
|
|
|
return #self.text_lines
|
|
|
|
end
|
|
|
|
|
|
|
|
function Label:getTextWidth()
|
|
|
|
render_text(self)
|
|
|
|
return self.text_width
|
|
|
|
end
|
|
|
|
|
|
|
|
function Label:onRenderBody(dc)
|
2016-05-09 19:29:04 -06:00
|
|
|
local text_pen = self.text_pen
|
|
|
|
if self:getMousePos() and (self.on_click or self.on_rclick) then
|
|
|
|
text_pen = self.text_hpen
|
|
|
|
end
|
|
|
|
render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self))
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
2022-10-07 13:45:43 -06:00
|
|
|
function Label:on_scrollbar(scroll_spec)
|
|
|
|
local v = 0
|
2022-10-21 13:17:59 -06:00
|
|
|
if tonumber(scroll_spec) then
|
|
|
|
v = scroll_spec - self.start_line_num
|
|
|
|
elseif scroll_spec == 'down_large' then
|
2022-10-07 13:45:43 -06:00
|
|
|
v = '+halfpage'
|
|
|
|
elseif scroll_spec == 'up_large' then
|
|
|
|
v = '-halfpage'
|
|
|
|
elseif scroll_spec == 'down_small' then
|
|
|
|
v = 1
|
|
|
|
elseif scroll_spec == 'up_small' then
|
|
|
|
v = -1
|
2022-09-11 20:33:01 -06:00
|
|
|
end
|
|
|
|
|
2022-10-07 13:45:43 -06:00
|
|
|
self:scroll(v)
|
2022-09-11 20:33:01 -06:00
|
|
|
end
|
|
|
|
|
2020-11-04 19:06:50 -07:00
|
|
|
function Label:scroll(nlines)
|
2022-09-11 20:33:01 -06:00
|
|
|
if not nlines then return end
|
2022-08-07 00:48:25 -06:00
|
|
|
if type(nlines) == 'string' then
|
|
|
|
if nlines == '+page' then
|
|
|
|
nlines = self.frame_body.height
|
|
|
|
elseif nlines == '-page' then
|
|
|
|
nlines = -self.frame_body.height
|
|
|
|
elseif nlines == '+halfpage' then
|
|
|
|
nlines = math.ceil(self.frame_body.height/2)
|
|
|
|
elseif nlines == '-halfpage' then
|
|
|
|
nlines = -math.ceil(self.frame_body.height/2)
|
|
|
|
else
|
|
|
|
error(('unhandled scroll keyword: "%s"'):format(nlines))
|
|
|
|
end
|
|
|
|
end
|
2020-11-04 19:06:50 -07:00
|
|
|
local n = self.start_line_num + nlines
|
2022-11-28 16:12:22 -07:00
|
|
|
local text_height = math.max(1, self:getTextHeight())
|
|
|
|
n = math.min(n, text_height - self.frame_body.height + 1)
|
2020-11-04 19:06:50 -07:00
|
|
|
n = math.max(n, 1)
|
2022-10-07 13:45:43 -06:00
|
|
|
nlines = n - self.start_line_num
|
2020-11-04 19:06:50 -07:00
|
|
|
self.start_line_num = n
|
2022-10-07 13:45:43 -06:00
|
|
|
update_label_scrollbar(self)
|
2022-09-11 20:33:01 -06:00
|
|
|
return nlines
|
2020-11-04 19:06:50 -07:00
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function Label:onInput(keys)
|
2020-11-04 19:06:50 -07:00
|
|
|
if is_disabled(self) then return false end
|
2022-10-07 13:45:43 -06:00
|
|
|
if self:inputToSubviews(keys) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then
|
|
|
|
self:on_click()
|
|
|
|
return true
|
2020-11-04 19:06:50 -07:00
|
|
|
end
|
|
|
|
if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then
|
|
|
|
self:on_rclick()
|
2022-10-07 13:45:43 -06:00
|
|
|
return true
|
2020-11-04 19:06:50 -07:00
|
|
|
end
|
|
|
|
for k,v in pairs(self.scroll_keys) do
|
2022-10-07 13:45:43 -06:00
|
|
|
if keys[k] and 0 ~= self:scroll(v) then
|
|
|
|
return true
|
2016-05-09 19:29:04 -06:00
|
|
|
end
|
2012-10-24 09:25:06 -06:00
|
|
|
end
|
2020-11-04 19:06:50 -07:00
|
|
|
return check_text_keys(self, keys)
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
2022-04-17 21:07:47 -06:00
|
|
|
------------------
|
2022-04-22 10:53:10 -06:00
|
|
|
-- WrappedLabel --
|
2022-04-17 21:07:47 -06:00
|
|
|
------------------
|
|
|
|
|
2022-04-22 10:53:10 -06:00
|
|
|
WrappedLabel = defclass(WrappedLabel, Label)
|
2022-04-17 21:07:47 -06:00
|
|
|
|
2022-04-22 10:53:10 -06:00
|
|
|
WrappedLabel.ATTRS{
|
|
|
|
text_to_wrap=DEFAULT_NIL,
|
|
|
|
indent=0,
|
2022-04-17 21:07:47 -06:00
|
|
|
}
|
|
|
|
|
2022-04-22 10:53:10 -06:00
|
|
|
function WrappedLabel:getWrappedText(width)
|
2022-04-29 12:29:00 -06:00
|
|
|
-- 0 width can happen if the parent has 0 width
|
|
|
|
if not self.text_to_wrap or width <= 0 then return nil end
|
2022-04-22 10:53:10 -06:00
|
|
|
local text_to_wrap = getval(self.text_to_wrap)
|
|
|
|
if type(text_to_wrap) == 'table' then
|
|
|
|
text_to_wrap = table.concat(text_to_wrap, NEWLINE)
|
2022-04-17 21:07:47 -06:00
|
|
|
end
|
2022-04-22 10:53:10 -06:00
|
|
|
return text_to_wrap:wrap(width - self.indent)
|
2022-04-17 21:07:47 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
-- we can't set the text in init() since we may not yet have a frame that we
|
|
|
|
-- can get wrapping bounds from.
|
2022-04-22 10:53:10 -06:00
|
|
|
function WrappedLabel:postComputeFrame()
|
2022-10-07 13:45:43 -06:00
|
|
|
local wrapped_text = self:getWrappedText(self.frame_body.width-1)
|
2022-04-22 10:53:10 -06:00
|
|
|
if not wrapped_text then return end
|
2022-04-17 21:07:47 -06:00
|
|
|
local text = {}
|
2022-04-22 10:53:10 -06:00
|
|
|
for _,line in ipairs(wrapped_text:split(NEWLINE)) do
|
2022-04-17 21:07:47 -06:00
|
|
|
table.insert(text, {gap=self.indent, text=line})
|
2022-04-22 10:53:10 -06:00
|
|
|
-- a trailing newline will get ignored so we don't have to manually trim
|
2022-04-17 21:07:47 -06:00
|
|
|
table.insert(text, NEWLINE)
|
|
|
|
end
|
|
|
|
self:setText(text)
|
|
|
|
end
|
|
|
|
|
2022-04-22 10:53:10 -06:00
|
|
|
------------------
|
|
|
|
-- TooltipLabel --
|
|
|
|
------------------
|
|
|
|
|
|
|
|
TooltipLabel = defclass(TooltipLabel, WrappedLabel)
|
|
|
|
|
|
|
|
TooltipLabel.ATTRS{
|
|
|
|
show_tooltip=DEFAULT_NIL,
|
|
|
|
indent=2,
|
|
|
|
text_pen=COLOR_GREY,
|
|
|
|
}
|
|
|
|
|
2022-12-02 16:36:45 -07:00
|
|
|
function TooltipLabel:init()
|
|
|
|
self.visible = self.show_tooltip
|
2022-04-22 10:53:10 -06:00
|
|
|
end
|
|
|
|
|
2022-04-17 21:07:47 -06:00
|
|
|
-----------------
|
|
|
|
-- HotkeyLabel --
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
HotkeyLabel = defclass(HotkeyLabel, Label)
|
|
|
|
|
|
|
|
HotkeyLabel.ATTRS{
|
|
|
|
key=DEFAULT_NIL,
|
2022-05-18 17:35:06 -06:00
|
|
|
key_sep=': ',
|
2022-04-17 21:07:47 -06:00
|
|
|
label=DEFAULT_NIL,
|
|
|
|
on_activate=DEFAULT_NIL,
|
|
|
|
}
|
|
|
|
|
|
|
|
function HotkeyLabel:init()
|
2022-05-18 17:35:06 -06:00
|
|
|
self:setText{{key=self.key, key_sep=self.key_sep, text=self.label,
|
|
|
|
on_activate=self.on_activate}}
|
2022-04-17 21:07:47 -06:00
|
|
|
end
|
|
|
|
|
2022-07-17 17:04:36 -06:00
|
|
|
function HotkeyLabel:onInput(keys)
|
|
|
|
if HotkeyLabel.super.onInput(self, keys) then
|
|
|
|
return true
|
2022-11-28 16:12:22 -07:00
|
|
|
elseif keys._MOUSE_L_DOWN and self:getMousePos() and self.on_activate then
|
2022-07-17 17:04:36 -06:00
|
|
|
self.on_activate()
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-04-17 21:07:47 -06:00
|
|
|
----------------------
|
|
|
|
-- CycleHotkeyLabel --
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
CycleHotkeyLabel = defclass(CycleHotkeyLabel, Label)
|
|
|
|
|
|
|
|
CycleHotkeyLabel.ATTRS{
|
|
|
|
key=DEFAULT_NIL,
|
|
|
|
label=DEFAULT_NIL,
|
|
|
|
label_width=DEFAULT_NIL,
|
|
|
|
options=DEFAULT_NIL,
|
|
|
|
initial_option=1,
|
|
|
|
on_change=DEFAULT_NIL,
|
|
|
|
}
|
|
|
|
|
|
|
|
function CycleHotkeyLabel:init()
|
|
|
|
-- initialize option_idx
|
|
|
|
for i in ipairs(self.options) do
|
|
|
|
if self.initial_option == self:getOptionValue(i) then
|
|
|
|
self.option_idx = i
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2022-05-18 15:04:56 -06:00
|
|
|
if not self.option_idx then
|
|
|
|
if self.options[self.initial_option] then
|
|
|
|
self.option_idx = self.initial_option
|
|
|
|
end
|
|
|
|
end
|
2022-04-17 21:07:47 -06:00
|
|
|
if not self.option_idx then
|
|
|
|
error(('cannot find option with value or index: "%s"')
|
|
|
|
:format(self.initial_option))
|
|
|
|
end
|
|
|
|
|
|
|
|
self:setText{
|
|
|
|
{key=self.key, key_sep=': ', text=self.label, width=self.label_width,
|
|
|
|
on_activate=self:callback('cycle')},
|
|
|
|
' ',
|
|
|
|
{text=self:callback('getOptionLabel')},
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
function CycleHotkeyLabel:cycle()
|
|
|
|
local old_option_idx = self.option_idx
|
|
|
|
if self.option_idx == #self.options then
|
|
|
|
self.option_idx = 1
|
|
|
|
else
|
|
|
|
self.option_idx = self.option_idx + 1
|
|
|
|
end
|
|
|
|
if self.on_change then
|
|
|
|
self.on_change(self:getOptionValue(),
|
|
|
|
self:getOptionValue(old_option_idx))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function CycleHotkeyLabel:getOptionLabel(option_idx)
|
|
|
|
option_idx = option_idx or self.option_idx
|
|
|
|
local option = self.options[option_idx]
|
|
|
|
if type(option) == 'table' then
|
|
|
|
return option.label
|
|
|
|
end
|
|
|
|
return option
|
|
|
|
end
|
|
|
|
|
|
|
|
function CycleHotkeyLabel:getOptionValue(option_idx)
|
|
|
|
option_idx = option_idx or self.option_idx
|
|
|
|
local option = self.options[option_idx]
|
|
|
|
if type(option) == 'table' then
|
|
|
|
return option.value
|
|
|
|
end
|
|
|
|
return option
|
|
|
|
end
|
|
|
|
|
2022-08-08 11:56:17 -06:00
|
|
|
function CycleHotkeyLabel:onInput(keys)
|
|
|
|
if CycleHotkeyLabel.super.onInput(self, keys) then
|
|
|
|
return true
|
2022-11-21 18:36:46 -07:00
|
|
|
elseif keys._MOUSE_L_DOWN and self:getMousePos() then
|
2022-08-08 11:56:17 -06:00
|
|
|
self:cycle()
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-04-17 21:07:47 -06:00
|
|
|
-----------------------
|
|
|
|
-- ToggleHotkeyLabel --
|
|
|
|
-----------------------
|
|
|
|
|
|
|
|
ToggleHotkeyLabel = defclass(ToggleHotkeyLabel, CycleHotkeyLabel)
|
|
|
|
ToggleHotkeyLabel.ATTRS{
|
|
|
|
options={{label='On', value=true},
|
|
|
|
{label='Off', value=false}},
|
|
|
|
}
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
----------
|
|
|
|
-- List --
|
|
|
|
----------
|
|
|
|
|
|
|
|
List = defclass(List, Widget)
|
|
|
|
|
|
|
|
List.ATTRS{
|
|
|
|
text_pen = COLOR_CYAN,
|
|
|
|
cursor_pen = COLOR_LIGHTCYAN,
|
|
|
|
inactive_pen = DEFAULT_NIL,
|
|
|
|
on_select = DEFAULT_NIL,
|
|
|
|
on_submit = DEFAULT_NIL,
|
2012-11-29 05:27:51 -07:00
|
|
|
on_submit2 = DEFAULT_NIL,
|
2012-10-16 04:18:35 -06:00
|
|
|
row_height = 1,
|
|
|
|
scroll_keys = STANDARDSCROLL,
|
2012-10-16 23:41:48 -06:00
|
|
|
icon_width = DEFAULT_NIL,
|
2012-10-16 04:18:35 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
function List:init(info)
|
|
|
|
self.page_top = 1
|
|
|
|
self.page_size = 1
|
2022-10-06 12:13:16 -06:00
|
|
|
self.scrollbar = Scrollbar{
|
|
|
|
frame={r=0},
|
|
|
|
on_scroll=self:callback('on_scrollbar')}
|
|
|
|
|
|
|
|
self:addviews{self.scrollbar}
|
2012-12-01 05:50:03 -07:00
|
|
|
|
|
|
|
if info.choices then
|
|
|
|
self:setChoices(info.choices, info.selected)
|
|
|
|
else
|
|
|
|
self.choices = {}
|
|
|
|
self.selected = 1
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function List:setChoices(choices, selected)
|
2018-06-20 08:23:57 -06:00
|
|
|
self.choices = {}
|
2012-10-16 04:18:35 -06:00
|
|
|
|
2018-06-20 08:46:24 -06:00
|
|
|
for i,v in ipairs(choices or {}) do
|
2018-06-20 08:23:57 -06:00
|
|
|
local l = utils.clone(v);
|
2012-10-16 04:18:35 -06:00
|
|
|
if type(v) ~= 'table' then
|
2018-06-20 08:23:57 -06:00
|
|
|
l = { text = v }
|
|
|
|
else
|
|
|
|
l.text = v.text or v.caption or v[1]
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
2018-06-20 08:23:57 -06:00
|
|
|
parse_label_text(l)
|
|
|
|
self.choices[i] = l
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
self:setSelected(selected)
|
|
|
|
end
|
|
|
|
|
|
|
|
function List:setSelected(selected)
|
|
|
|
self.selected = selected or self.selected or 1
|
|
|
|
self:moveCursor(0, true)
|
|
|
|
return self.selected
|
|
|
|
end
|
|
|
|
|
2012-10-17 00:41:50 -06:00
|
|
|
function List:getChoices()
|
|
|
|
return self.choices
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function List:getSelected()
|
2012-10-24 09:25:06 -06:00
|
|
|
if #self.choices > 0 then
|
|
|
|
return self.selected, self.choices[self.selected]
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function List:getContentWidth()
|
|
|
|
local width = 0
|
|
|
|
for i,v in ipairs(self.choices) do
|
|
|
|
render_text(v)
|
|
|
|
local roww = v.text_width
|
|
|
|
if v.key then
|
|
|
|
roww = roww + 3 + #gui.getKeyDisplay(v.key)
|
|
|
|
end
|
|
|
|
width = math.max(width, roww)
|
|
|
|
end
|
2012-10-16 23:41:48 -06:00
|
|
|
return width + (self.icon_width or 0)
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function List:getContentHeight()
|
|
|
|
return #self.choices * self.row_height
|
|
|
|
end
|
|
|
|
|
|
|
|
function List:postComputeFrame(body)
|
|
|
|
self.page_size = math.max(1, math.floor(body.height / self.row_height))
|
|
|
|
self:moveCursor(0)
|
|
|
|
end
|
|
|
|
|
2022-10-06 12:13:16 -06:00
|
|
|
local function update_list_scrollbar(list)
|
2022-10-07 13:45:43 -06:00
|
|
|
list.scrollbar:update(list.page_top, list.page_size, #list.choices)
|
|
|
|
end
|
|
|
|
|
|
|
|
function List:postUpdateLayout()
|
|
|
|
update_list_scrollbar(self)
|
2022-10-06 12:13:16 -06:00
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function List:moveCursor(delta, force_cb)
|
|
|
|
local cnt = #self.choices
|
2012-10-16 08:33:00 -06:00
|
|
|
|
|
|
|
if cnt < 1 then
|
|
|
|
self.page_top = 1
|
|
|
|
self.selected = 1
|
2022-10-06 12:13:16 -06:00
|
|
|
update_list_scrollbar(self)
|
2012-12-01 05:50:03 -07:00
|
|
|
if force_cb and self.on_select then
|
|
|
|
self.on_select(nil,nil)
|
|
|
|
end
|
2012-10-16 08:33:00 -06:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
local off = self.selected+delta-1
|
|
|
|
local ds = math.abs(delta)
|
|
|
|
|
|
|
|
if ds > 1 then
|
|
|
|
if off >= cnt+ds-1 then
|
|
|
|
off = 0
|
|
|
|
else
|
|
|
|
off = math.min(cnt-1, off)
|
|
|
|
end
|
|
|
|
if off <= -ds then
|
|
|
|
off = cnt-1
|
|
|
|
else
|
|
|
|
off = math.max(0, off)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-06 12:13:16 -06:00
|
|
|
local buffer = 1 + math.min(4, math.floor(self.page_size/10))
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
self.selected = 1 + off % cnt
|
2022-10-06 12:13:16 -06:00
|
|
|
if (self.selected - buffer) < self.page_top then
|
|
|
|
self.page_top = math.max(1, self.selected - buffer)
|
|
|
|
elseif (self.selected + buffer + 1) > (self.page_top + self.page_size) then
|
|
|
|
local max_page_top = cnt - self.page_size + 1
|
|
|
|
self.page_top = math.max(1,
|
|
|
|
math.min(max_page_top, self.selected - self.page_size + buffer + 1))
|
|
|
|
end
|
|
|
|
update_list_scrollbar(self)
|
2012-10-16 04:18:35 -06:00
|
|
|
|
|
|
|
if (force_cb or delta ~= 0) and self.on_select then
|
|
|
|
self.on_select(self:getSelected())
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-06 12:13:16 -06:00
|
|
|
function List:on_scrollbar(scroll_spec)
|
|
|
|
local v = 0
|
2022-10-21 13:17:59 -06:00
|
|
|
if tonumber(scroll_spec) then
|
|
|
|
v = scroll_spec - self.page_top
|
|
|
|
elseif scroll_spec == 'down_large' then
|
2022-10-07 13:45:43 -06:00
|
|
|
v = math.ceil(self.page_size / 2)
|
2022-10-06 12:13:16 -06:00
|
|
|
elseif scroll_spec == 'up_large' then
|
2022-10-07 13:45:43 -06:00
|
|
|
v = -math.ceil(self.page_size / 2)
|
2022-10-06 12:13:16 -06:00
|
|
|
elseif scroll_spec == 'down_small' then
|
|
|
|
v = 1
|
|
|
|
elseif scroll_spec == 'up_small' then
|
|
|
|
v = -1
|
|
|
|
end
|
|
|
|
|
|
|
|
local max_page_top = math.max(1, #self.choices - self.page_size + 1)
|
|
|
|
self.page_top = math.max(1, math.min(max_page_top, self.page_top + v))
|
|
|
|
update_list_scrollbar(self)
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function List:onRenderBody(dc)
|
|
|
|
local choices = self.choices
|
|
|
|
local top = self.page_top
|
|
|
|
local iend = math.min(#choices, top+self.page_size-1)
|
2012-10-16 23:41:48 -06:00
|
|
|
local iw = self.icon_width
|
2012-10-16 04:18:35 -06:00
|
|
|
|
2012-11-17 09:32:39 -07:00
|
|
|
local function paint_icon(icon, obj)
|
|
|
|
if type(icon) ~= 'string' then
|
|
|
|
dc:char(nil,icon)
|
|
|
|
else
|
|
|
|
if current then
|
|
|
|
dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen)
|
|
|
|
else
|
|
|
|
dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
for i = top,iend do
|
|
|
|
local obj = choices[i]
|
|
|
|
local current = (i == self.selected)
|
2012-10-24 09:25:06 -06:00
|
|
|
local cur_pen = self.cursor_pen
|
|
|
|
local cur_dpen = self.text_pen
|
2015-10-30 17:16:29 -06:00
|
|
|
local active_pen = current and cur_pen or cur_dpen
|
2012-10-16 04:18:35 -06:00
|
|
|
|
2022-12-02 16:36:45 -07:00
|
|
|
if not getval(self.active) then
|
2012-10-16 04:18:35 -06:00
|
|
|
cur_pen = self.inactive_pen or self.cursor_pen
|
|
|
|
end
|
|
|
|
|
|
|
|
local y = (i - top)*self.row_height
|
2012-11-17 09:32:39 -07:00
|
|
|
local icon = getval(obj.icon)
|
2012-10-16 23:41:48 -06:00
|
|
|
|
2012-11-17 09:32:39 -07:00
|
|
|
if iw and icon then
|
2015-10-30 17:16:29 -06:00
|
|
|
dc:seek(0, y):pen(active_pen)
|
2012-11-17 09:32:39 -07:00
|
|
|
paint_icon(icon, obj)
|
2012-10-16 23:41:48 -06:00
|
|
|
end
|
|
|
|
|
2012-10-24 09:25:06 -06:00
|
|
|
render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current)
|
2012-10-16 04:18:35 -06:00
|
|
|
|
2012-11-17 09:32:39 -07:00
|
|
|
local ip = dc.width
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
if obj.key then
|
|
|
|
local keystr = gui.getKeyDisplay(obj.key)
|
2022-10-19 18:30:51 -06:00
|
|
|
ip = ip-3-#keystr
|
2012-11-17 09:32:39 -07:00
|
|
|
dc:seek(ip,y):pen(self.text_pen)
|
2012-10-16 04:18:35 -06:00
|
|
|
dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')')
|
|
|
|
end
|
2012-11-17 09:32:39 -07:00
|
|
|
|
|
|
|
if icon and not iw then
|
2015-10-30 17:16:29 -06:00
|
|
|
dc:seek(ip-1,y):pen(active_pen)
|
2012-11-17 09:32:39 -07:00
|
|
|
paint_icon(icon, obj)
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-11-07 17:13:45 -07:00
|
|
|
function List:getIdxUnderMouse()
|
2022-11-11 11:40:10 -07:00
|
|
|
if self.scrollbar:getMousePos() then return end
|
2022-11-07 17:13:45 -07:00
|
|
|
local _,mouse_y = self:getMousePos()
|
|
|
|
if mouse_y and #self.choices > 0 and
|
|
|
|
mouse_y < (#self.choices-self.page_top+1) * self.row_height then
|
|
|
|
return self.page_top + math.floor(mouse_y/self.row_height)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-16 08:33:00 -06:00
|
|
|
function List:submit()
|
|
|
|
if self.on_submit and #self.choices > 0 then
|
|
|
|
self.on_submit(self:getSelected())
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-11-29 05:27:51 -07:00
|
|
|
function List:submit2()
|
|
|
|
if self.on_submit2 and #self.choices > 0 then
|
|
|
|
self.on_submit2(self:getSelected())
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-16 04:18:35 -06:00
|
|
|
function List:onInput(keys)
|
2022-10-06 12:13:16 -06:00
|
|
|
if self:inputToSubviews(keys) then
|
|
|
|
return true
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
if self.on_submit and keys.SELECT then
|
2012-10-16 08:33:00 -06:00
|
|
|
self:submit()
|
2012-10-16 04:18:35 -06:00
|
|
|
return true
|
2012-11-29 05:27:51 -07:00
|
|
|
elseif self.on_submit2 and keys.SEC_SELECT then
|
|
|
|
self:submit2()
|
|
|
|
return true
|
2022-11-21 18:36:46 -07:00
|
|
|
elseif keys._MOUSE_L_DOWN then
|
2022-11-07 17:13:45 -07:00
|
|
|
local idx = self:getIdxUnderMouse()
|
|
|
|
if idx then
|
2022-07-16 23:18:38 -06:00
|
|
|
self:setSelected(idx)
|
2022-11-07 17:14:16 -07:00
|
|
|
if dfhack.internal.getModifiers().shift then
|
|
|
|
self:submit2()
|
|
|
|
else
|
|
|
|
self:submit()
|
|
|
|
end
|
2022-07-16 23:18:38 -06:00
|
|
|
return true
|
|
|
|
end
|
2012-10-16 04:18:35 -06:00
|
|
|
else
|
|
|
|
for k,v in pairs(self.scroll_keys) do
|
|
|
|
if keys[k] then
|
|
|
|
if v == '+page' then
|
|
|
|
v = self.page_size
|
|
|
|
elseif v == '-page' then
|
|
|
|
v = -self.page_size
|
|
|
|
end
|
|
|
|
|
|
|
|
self:moveCursor(v)
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
for i,v in ipairs(self.choices) do
|
|
|
|
if v.key and keys[v.key] then
|
|
|
|
self:setSelected(i)
|
2012-10-16 08:33:00 -06:00
|
|
|
self:submit()
|
2012-10-16 04:18:35 -06:00
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local current = self.choices[self.selected]
|
|
|
|
if current then
|
|
|
|
return check_text_keys(current, keys)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-17 00:41:50 -06:00
|
|
|
-------------------
|
|
|
|
-- Filtered List --
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
FilteredList = defclass(FilteredList, Widget)
|
|
|
|
|
2012-11-30 08:10:17 -07:00
|
|
|
FilteredList.ATTRS {
|
|
|
|
edit_below = false,
|
2017-06-27 19:10:14 -06:00
|
|
|
edit_key = DEFAULT_NIL,
|
2022-08-29 12:40:56 -06:00
|
|
|
edit_ignore_keys = DEFAULT_NIL,
|
2022-10-21 13:43:53 -06:00
|
|
|
edit_on_char = DEFAULT_NIL,
|
2012-11-30 08:10:17 -07:00
|
|
|
}
|
|
|
|
|
2012-10-17 00:41:50 -06:00
|
|
|
function FilteredList:init(info)
|
2022-10-21 13:43:53 -06:00
|
|
|
local on_char = self:callback('onFilterChar')
|
|
|
|
if self.edit_on_char then
|
|
|
|
on_char = function(c, text)
|
|
|
|
return self.edit_on_char(c, text) and self:onFilterChar(c, text)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-10-17 00:41:50 -06:00
|
|
|
self.edit = EditField{
|
2012-11-04 06:06:32 -07:00
|
|
|
text_pen = info.edit_pen or info.cursor_pen,
|
2012-11-30 08:10:17 -07:00
|
|
|
frame = { l = info.icon_width, t = 0, h = 1 },
|
2012-10-17 00:41:50 -06:00
|
|
|
on_change = self:callback('onFilterChange'),
|
2022-10-21 13:43:53 -06:00
|
|
|
on_char = on_char,
|
2017-06-27 19:10:14 -06:00
|
|
|
key = self.edit_key,
|
2022-08-29 12:40:56 -06:00
|
|
|
ignore_keys = self.edit_ignore_keys,
|
2012-10-17 00:41:50 -06:00
|
|
|
}
|
|
|
|
self.list = List{
|
|
|
|
frame = { t = 2 },
|
|
|
|
text_pen = info.text_pen,
|
|
|
|
cursor_pen = info.cursor_pen,
|
|
|
|
inactive_pen = info.inactive_pen,
|
2012-11-04 06:06:32 -07:00
|
|
|
icon_pen = info.icon_pen,
|
2012-10-17 00:41:50 -06:00
|
|
|
row_height = info.row_height,
|
|
|
|
scroll_keys = info.scroll_keys,
|
|
|
|
icon_width = info.icon_width,
|
|
|
|
}
|
2012-11-30 08:10:17 -07:00
|
|
|
if self.edit_below then
|
|
|
|
self.edit.frame = { l = info.icon_width, b = 0, h = 1 }
|
|
|
|
self.list.frame = { t = 0, b = 2 }
|
|
|
|
end
|
2012-10-17 00:41:50 -06:00
|
|
|
if info.on_select then
|
|
|
|
self.list.on_select = function()
|
|
|
|
return info.on_select(self:getSelected())
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if info.on_submit then
|
|
|
|
self.list.on_submit = function()
|
|
|
|
return info.on_submit(self:getSelected())
|
|
|
|
end
|
|
|
|
end
|
2012-11-29 05:27:51 -07:00
|
|
|
if info.on_submit2 then
|
|
|
|
self.list.on_submit2 = function()
|
|
|
|
return info.on_submit2(self:getSelected())
|
|
|
|
end
|
|
|
|
end
|
2012-10-17 00:41:50 -06:00
|
|
|
self.not_found = Label{
|
2012-12-01 05:50:03 -07:00
|
|
|
visible = true,
|
2012-10-17 00:41:50 -06:00
|
|
|
text = info.not_found_label or 'No matches',
|
|
|
|
text_pen = COLOR_LIGHTRED,
|
2012-11-30 08:10:17 -07:00
|
|
|
frame = { l = info.icon_width, t = self.list.frame.t },
|
2012-10-17 00:41:50 -06:00
|
|
|
}
|
2012-10-17 01:49:11 -06:00
|
|
|
self:addviews{ self.edit, self.list, self.not_found }
|
2012-12-01 05:50:03 -07:00
|
|
|
if info.choices then
|
|
|
|
self:setChoices(info.choices, info.selected)
|
|
|
|
else
|
|
|
|
self.choices = {}
|
|
|
|
end
|
2012-10-17 00:41:50 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:getChoices()
|
|
|
|
return self.choices
|
|
|
|
end
|
|
|
|
|
2017-06-27 19:10:14 -06:00
|
|
|
function FilteredList:getVisibleChoices()
|
|
|
|
return self.list.choices
|
|
|
|
end
|
|
|
|
|
2012-10-17 00:41:50 -06:00
|
|
|
function FilteredList:setChoices(choices, pos)
|
|
|
|
choices = choices or {}
|
2022-08-15 17:22:56 -06:00
|
|
|
self.edit:setText('')
|
2012-10-17 00:41:50 -06:00
|
|
|
self.list:setChoices(choices, pos)
|
2020-02-26 23:56:30 -07:00
|
|
|
self.choices = self.list.choices
|
2012-10-17 00:41:50 -06:00
|
|
|
self.not_found.visible = (#choices == 0)
|
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:submit()
|
|
|
|
return self.list:submit()
|
|
|
|
end
|
|
|
|
|
2012-11-29 05:27:51 -07:00
|
|
|
function FilteredList:submit2()
|
|
|
|
return self.list:submit2()
|
|
|
|
end
|
|
|
|
|
2012-10-17 00:41:50 -06:00
|
|
|
function FilteredList:canSubmit()
|
|
|
|
return not self.not_found.visible
|
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:getSelected()
|
|
|
|
local i,v = self.list:getSelected()
|
2012-10-24 09:25:06 -06:00
|
|
|
if i then
|
|
|
|
return map_opttab(self.choice_index, i), v
|
|
|
|
end
|
2012-10-17 00:41:50 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:getContentWidth()
|
|
|
|
return self.list:getContentWidth()
|
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:getContentHeight()
|
|
|
|
return self.list:getContentHeight() + 2
|
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:getFilter()
|
|
|
|
return self.edit.text, self.list.choices
|
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:setFilter(filter, pos)
|
|
|
|
local choices = self.choices
|
|
|
|
local cidx = nil
|
|
|
|
|
|
|
|
filter = filter or ''
|
2022-08-29 16:17:17 -06:00
|
|
|
if filter ~= self.edit.text then
|
|
|
|
self.edit:setText(filter)
|
|
|
|
end
|
2012-10-17 00:41:50 -06:00
|
|
|
|
|
|
|
if filter ~= '' then
|
2021-07-03 00:30:59 -06:00
|
|
|
local tokens = filter:split()
|
2012-10-17 00:41:50 -06:00
|
|
|
local ipos = pos
|
|
|
|
|
|
|
|
choices = {}
|
|
|
|
cidx = {}
|
|
|
|
pos = nil
|
|
|
|
|
|
|
|
for i,v in ipairs(self.choices) do
|
|
|
|
local ok = true
|
2022-11-28 16:49:01 -07:00
|
|
|
local search_key = v.search_key
|
|
|
|
if not search_key then
|
|
|
|
if type(v.text) ~= 'table' then
|
|
|
|
search_key = v.text
|
|
|
|
else
|
|
|
|
local texts = {}
|
|
|
|
for _,token in ipairs(v.text) do
|
|
|
|
table.insert(texts,
|
|
|
|
type(token) == 'string' and token
|
|
|
|
or getval(token.text) or '')
|
|
|
|
end
|
|
|
|
search_key = table.concat(texts, ' ')
|
|
|
|
end
|
|
|
|
end
|
2012-10-17 00:41:50 -06:00
|
|
|
for _,key in ipairs(tokens) do
|
2022-04-11 19:22:31 -06:00
|
|
|
key = key:escape_pattern()
|
|
|
|
-- start matches at non-space or non-punctuation. this allows
|
|
|
|
-- punctuation itself to be matched if that is useful (e.g.
|
|
|
|
-- filenames or parameter names)
|
2020-08-17 22:15:33 -06:00
|
|
|
if key ~= '' and
|
2022-04-11 19:22:31 -06:00
|
|
|
not search_key:match('%f[^%p\x00]'..key) and
|
|
|
|
not search_key:match('%f[^%s\x00]'..key) then
|
2012-10-17 00:41:50 -06:00
|
|
|
ok = false
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if ok then
|
|
|
|
table.insert(choices, v)
|
|
|
|
cidx[#choices] = i
|
|
|
|
if ipos == i then
|
|
|
|
pos = #choices
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
self.choice_index = cidx
|
|
|
|
self.list:setChoices(choices, pos)
|
|
|
|
self.not_found.visible = (#choices == 0)
|
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:onFilterChange(text)
|
|
|
|
self:setFilter(text)
|
|
|
|
end
|
|
|
|
|
|
|
|
function FilteredList:onFilterChar(char, text)
|
|
|
|
if char == ' ' then
|
|
|
|
return string.match(text, '%S$')
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2012-10-15 10:03:18 -06:00
|
|
|
return _ENV
|