wolfboyft

widgets

Oct 24th, 2022 (edited)
116
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 40.33 KB | None | 0 0
  1. -- Simple widgets for screens
  2.  
  3. local _ENV = mkmodule('gui.widgets')
  4.  
  5. local gui = require('gui')
  6. local utils = require('utils')
  7.  
  8. local dscreen = dfhack.screen
  9.  
  10. local function show_view(view,vis)
  11.     if view then
  12.         view.visible = vis
  13.     end
  14. end
  15.  
  16. local function getval(obj)
  17.     if type(obj) == 'function' then
  18.         return obj()
  19.     else
  20.         return obj
  21.     end
  22. end
  23.  
  24. local function map_opttab(tab,idx)
  25.     if tab then
  26.         return tab[idx]
  27.     else
  28.         return idx
  29.     end
  30. end
  31.  
  32. STANDARDSCROLL = {
  33.     STANDARDSCROLL_UP = -1,
  34.     STANDARDSCROLL_DOWN = 1,
  35.     STANDARDSCROLL_PAGEUP = '-page',
  36.     STANDARDSCROLL_PAGEDOWN = '+page',
  37. }
  38.  
  39. SECONDSCROLL = {
  40.     SECONDSCROLL_UP = -1,
  41.     SECONDSCROLL_DOWN = 1,
  42.     SECONDSCROLL_PAGEUP = '-page',
  43.     SECONDSCROLL_PAGEDOWN = '+page',
  44. }
  45.  
  46. ------------
  47. -- Widget --
  48. ------------
  49.  
  50. Widget = defclass(Widget, gui.View)
  51.  
  52. Widget.ATTRS {
  53.     frame = DEFAULT_NIL,
  54.     frame_inset = DEFAULT_NIL,
  55.     frame_background = DEFAULT_NIL,
  56. }
  57.  
  58. function Widget:computeFrame(parent_rect)
  59.     local sw, sh = parent_rect.width, parent_rect.height
  60.     return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset)
  61. end
  62.  
  63. function Widget:onRenderFrame(dc, rect)
  64.     if self.frame_background then
  65.         dc:fill(rect, self.frame_background)
  66.     end
  67. end
  68.  
  69. -----------
  70. -- Panel --
  71. -----------
  72.  
  73. Panel = defclass(Panel, Widget)
  74.  
  75. Panel.ATTRS {
  76.     on_render = DEFAULT_NIL,
  77.     on_layout = DEFAULT_NIL,
  78.     autoarrange_subviews = false, -- whether to automatically lay out subviews
  79.     autoarrange_gap = 0, -- how many blank lines to insert between widgets
  80. }
  81.  
  82. function Panel:init(args)
  83.     self:addviews(args.subviews)
  84. end
  85.  
  86. function Panel:onRenderBody(dc)
  87.     if self.on_render then self.on_render(dc) end
  88. end
  89.  
  90. function Panel:postComputeFrame(body)
  91.     if self.on_layout then self.on_layout(body) end
  92. end
  93.  
  94. -- if self.autoarrange_subviews is true, lay out visible subviews vertically,
  95. -- adding gaps between widgets according to self.autoarrange_gap.
  96. function Panel:postUpdateLayout()
  97.     if not self.autoarrange_subviews then return end
  98.  
  99.     local gap = self.autoarrange_gap
  100.     local y = 0
  101.     for _,subview in ipairs(self.subviews) do
  102.         if not subview.frame then goto continue end
  103.         subview.frame.t = y
  104.         if subview.visible then
  105.             y = y + (subview.frame.h or 0) + gap
  106.         end
  107.         ::continue::
  108.     end
  109.     self.frame_rect.height = y
  110.  
  111.     -- let widgets adjust to their new positions
  112.     self:updateSubviewLayout()
  113. end
  114.  
  115. -------------------
  116. -- ResizingPanel --
  117. -------------------
  118.  
  119. ResizingPanel = defclass(ResizingPanel, Panel)
  120.  
  121. -- adjust our frame dimensions according to positions and sizes of our subviews
  122. function ResizingPanel:postUpdateLayout(frame_body)
  123.     local w, h = 0, 0
  124.     for _,s in ipairs(self.subviews) do
  125.         if s.visible then
  126.             w = math.max(w, (s.frame and s.frame.l or 0) +
  127.                             (s.frame and s.frame.w or frame_body.width))
  128.             h = math.max(h, (s.frame and s.frame.t or 0) +
  129.                             (s.frame and s.frame.h or frame_body.height))
  130.         end
  131.     end
  132.     if not self.frame then self.frame = {} end
  133.     self.frame.w, self.frame.h = w, h
  134. end
  135.  
  136. -----------
  137. -- Pages --
  138. -----------
  139.  
  140. Pages = defclass(Pages, Panel)
  141.  
  142. function Pages:init(args)
  143.     for _,v in ipairs(self.subviews) do
  144.         v.visible = false
  145.     end
  146.     self:setSelected(args.selected or 1)
  147. end
  148.  
  149. function Pages:setSelected(idx)
  150.     if type(idx) ~= 'number' then
  151.         local key = idx
  152.         if type(idx) == 'string' then
  153.             key = self.subviews[key]
  154.         end
  155.         idx = utils.linear_index(self.subviews, key)
  156.         if not idx then
  157.             error('Unknown page: '..key)
  158.         end
  159.     end
  160.  
  161.     show_view(self.subviews[self.selected], false)
  162.     self.selected = math.min(math.max(1, idx), #self.subviews)
  163.     show_view(self.subviews[self.selected], true)
  164. end
  165.  
  166. function Pages:getSelected()
  167.     return self.selected, self.subviews[self.selected]
  168. end
  169.  
  170. function Pages:getSelectedPage()
  171.     return self.subviews[self.selected]
  172. end
  173.  
  174. ----------------
  175. -- Edit field --
  176. ----------------
  177.  
  178. EditField = defclass(EditField, Widget)
  179.  
  180. EditField.ATTRS{
  181.     label_text = DEFAULT_NIL,
  182.     text = '',
  183.     text_pen = DEFAULT_NIL,
  184.     on_char = DEFAULT_NIL,
  185.     on_change = DEFAULT_NIL,
  186.     on_submit = DEFAULT_NIL,
  187.     on_submit2 = DEFAULT_NIL,
  188.     key = DEFAULT_NIL,
  189.     key_sep = DEFAULT_NIL,
  190.     modal = false,
  191.     ignore_keys = DEFAULT_NIL,
  192. }
  193.  
  194. function EditField:preinit(init_table)
  195.     init_table.frame = init_table.frame or {}
  196.     init_table.frame.h = init_table.frame.h or 1
  197. end
  198.  
  199. function EditField:init()
  200.     local function on_activate()
  201.         self.saved_text = self.text
  202.         self:setFocus(true)
  203.     end
  204.  
  205.     self.start_pos = 1
  206.     self.cursor = #self.text + 1
  207.  
  208.     self:addviews{HotkeyLabel{frame={t=0,l=0},
  209.                               key=self.key,
  210.                               key_sep=self.key_sep,
  211.                               label=self.label_text,
  212.                               on_activate=self.key and on_activate or nil}}
  213. end
  214.  
  215. function EditField:getPreferredFocusState()
  216.     return not self.key
  217. end
  218.  
  219. function EditField:setCursor(cursor)
  220.     if not cursor or cursor > #self.text then
  221.         self.cursor = #self.text + 1
  222.         return
  223.     end
  224.     self.cursor = math.max(1, cursor)
  225. end
  226.  
  227. function EditField:setText(text, cursor)
  228.     self.text = text
  229.     self:setCursor(cursor)
  230. end
  231.  
  232. function EditField:postUpdateLayout()
  233.     self.text_offset = self.subviews[1]:getTextWidth()
  234. end
  235.  
  236. function EditField:onRenderBody(dc)
  237.     dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0)
  238.  
  239.     local cursor_char = '_'
  240.     if not self.active or not self.focus or gui.blink_visible(300) then
  241.         cursor_char = (self.cursor > #self.text) and ' ' or
  242.                                         self.text:sub(self.cursor, self.cursor)
  243.     end
  244.     local txt = self.text:sub(1, self.cursor - 1) .. cursor_char ..
  245.                                                 self.text:sub(self.cursor + 1)
  246.     local max_width = dc.width - self.text_offset
  247.     self.start_pos = 1
  248.     if #txt > max_width then
  249.         -- get the substring in the vicinity of the cursor
  250.         max_width = max_width - 2
  251.         local half_width = math.floor(max_width/2)
  252.         local start_pos = math.max(1, self.cursor-half_width)
  253.         local end_pos = math.min(#txt, self.cursor+half_width-1)
  254.         if self.cursor + half_width > #txt then
  255.             start_pos = #txt - (max_width - 1)
  256.         end
  257.         if self.cursor - half_width <= 1 then
  258.             end_pos = max_width + 1
  259.         end
  260.         self.start_pos = start_pos > 1 and start_pos - 1 or start_pos
  261.         txt = ('%s%s%s'):format(start_pos == 1 and '' or string.char(27),
  262.                                 txt:sub(start_pos, end_pos),
  263.                                 end_pos == #txt and '' or string.char(26))
  264.     end
  265.     dc:advance(self.text_offset):string(txt)
  266.     dc:string((' '):rep(dc.clip_x2 - dc.x))
  267. end
  268.  
  269. function EditField:onInput(keys)
  270.     if not self.focus then
  271.         -- only react to our hotkey
  272.         return self:inputToSubviews(keys)
  273.     end
  274.  
  275.     if self.ignore_keys then
  276.         for _,ignore_key in ipairs(self.ignore_keys) do
  277.             if keys[ignore_key] then return false end
  278.         end
  279.     end
  280.  
  281.     if self.key and keys.LEAVESCREEN then
  282.         local old = self.text
  283.         self:setText(self.saved_text)
  284.         if self.on_change and old ~= self.saved_text then
  285.             self.on_change(self.text, old)
  286.         end
  287.         self:setFocus(false)
  288.         return true
  289.     end
  290.  
  291.     if keys.SELECT then
  292.         if self.key then
  293.             self:setFocus(false)
  294.         end
  295.         if self.on_submit then
  296.             self.on_submit(self.text)
  297.             return true
  298.         end
  299.         return not not self.key
  300.     elseif keys.SEC_SELECT then
  301.         if self.key then
  302.             self:setFocus(false)
  303.         end
  304.         if self.on_submit2 then
  305.             self.on_submit2(self.text)
  306.             return true
  307.         end
  308.         return not not self.key
  309.     elseif keys._MOUSE_L then
  310.         local mouse_x, mouse_y = self:getMousePos()
  311.         if mouse_x then
  312.             self:setCursor(self.start_pos + mouse_x)
  313.             return true
  314.         end
  315.     elseif keys._STRING then
  316.         local old = self.text
  317.         if keys._STRING == 0 then
  318.             -- handle backspace
  319.             local del_pos = self.cursor - 1
  320.             if del_pos > 0 then
  321.                 self:setText(old:sub(1, del_pos-1) .. old:sub(del_pos+1),
  322.                              del_pos)
  323.             end
  324.         else
  325.             local cv = string.char(keys._STRING)
  326.             if not self.on_char or self.on_char(cv, old) then
  327.                 self:setText(old:sub(1,self.cursor-1)..cv..old:sub(self.cursor),
  328.                              self.cursor + 1)
  329.             end
  330.         end
  331.         if self.on_change and self.text ~= old then
  332.             self.on_change(self.text, old)
  333.         end
  334.         return true
  335.     elseif keys.CURSOR_LEFT then
  336.         self:setCursor(self.cursor - 1)
  337.         return true
  338.     elseif keys.A_MOVE_W_DOWN then -- Ctrl-Left (end of prev word)
  339.         local _, prev_word_end = self.text:sub(1, self.cursor-1):
  340.                                                     find('.*[%w_%-][^%w_%-]')
  341.         self:setCursor(prev_word_end or 1)
  342.         return true
  343.     elseif keys.A_CARE_MOVE_W then -- Alt-Left (home)
  344.         self:setCursor(1)
  345.         return true
  346.     elseif keys.CURSOR_RIGHT then
  347.         self:setCursor(self.cursor + 1)
  348.         return true
  349.     elseif keys.A_MOVE_E_DOWN then -- Ctrl-Right (beginning of next word)
  350.         local _,next_word_start = self.text:find('[^%w_%-][%w_%-]', self.cursor)
  351.         self:setCursor(next_word_start)
  352.         return true
  353.     elseif keys.A_CARE_MOVE_E then -- Alt-Right (end)
  354.         self:setCursor()
  355.         return true
  356.     end
  357.  
  358.     -- if we're modal, then unconditionally eat all the input
  359.     return self.modal
  360. end
  361.  
  362. ---------------
  363. -- Scrollbar --
  364. ---------------
  365.  
  366. -- these can be overridden by the user, e.g.:
  367. --   require('gui.widgets').SCROLL_DELAY_MS = 100
  368. SCROLL_INITIAL_DELAY_MS = 300
  369. SCROLL_DELAY_MS = 20
  370.  
  371. Scrollbar = defclass(Scrollbar, Widget)
  372.  
  373. Scrollbar.ATTRS{
  374.     fg = COLOR_LIGHTGREEN,
  375.     bg = COLOR_CYAN,
  376.     on_scroll = DEFAULT_NIL,
  377. }
  378.  
  379. function Scrollbar:preinit(init_table)
  380.     init_table.frame = init_table.frame or {}
  381.     init_table.frame.w = init_table.frame.w or 1
  382. end
  383.  
  384. function Scrollbar:init()
  385.     self.last_scroll_ms = 0
  386.     self.is_first_click = false
  387.     self.scroll_spec = nil
  388.     self.is_dragging = false -- index of the scrollbar tile that we're dragging
  389.     self:update(1, 1, 1)
  390. end
  391.  
  392. -- calculate and cache the number of tiles of empty space above the top of the
  393. -- scrollbar and the number of tiles the scrollbar should occupy to represent
  394. -- the percentage of text that is on the screen.
  395. -- if elems_per_page or num_elems are not specified, the last values passed to
  396. -- Scrollbar:update() are used.
  397. function Scrollbar:update(top_elem, elems_per_page, num_elems)
  398.     if not top_elem then error('must specify index of new top element') end
  399.     elems_per_page = elems_per_page or self.elems_per_page
  400.     num_elems = num_elems or self.num_elems
  401.  
  402.     local frame_height = self.frame_body and self.frame_body.height or 3
  403.     local scrollbar_body_height = frame_height - 2
  404.     local height = math.max(1, math.floor(
  405.         (math.min(elems_per_page, num_elems) * scrollbar_body_height) /
  406.         num_elems))
  407.  
  408.     local max_pos = scrollbar_body_height - height
  409.     local pos = (num_elems == elems_per_page) and 0 or
  410.             math.ceil(((top_elem-1) * max_pos) /
  411.                       (num_elems - elems_per_page))
  412.  
  413.     self.top_elem = top_elem
  414.     self.elems_per_page, self.num_elems = elems_per_page, num_elems
  415.     self.bar_offset, self.bar_height = pos, height
  416. end
  417.  
  418. local function scrollbar_do_drag(scrollbar)
  419.     local x,y = dfhack.screen.getMousePos()
  420.     x,y = scrollbar.frame_body:localXY(x,y)
  421.     local bar_idx = y - scrollbar.bar_offset
  422.     local delta = bar_idx - scrollbar.is_dragging
  423.     if delta < -scrollbar.bar_height then
  424.         scrollbar.on_scroll('up_large')
  425.     elseif delta < 0 then
  426.         scrollbar.on_scroll('up_small')
  427.     elseif delta > scrollbar.bar_height then
  428.         scrollbar.on_scroll('down_large')
  429.     elseif delta > 0 then
  430.         scrollbar.on_scroll('down_small')
  431.     end
  432. end
  433.  
  434. local UP_ARROW_CHAR = string.char(24)
  435. local DOWN_ARROW_CHAR = string.char(25)
  436. local NO_ARROW_CHAR = string.char(32)
  437. local BAR_CHAR = string.char(7)
  438. local BAR_BG_CHAR = string.char(179)
  439.  
  440. function Scrollbar:onRenderBody(dc)
  441.     -- don't draw if all elements are visible
  442.     if self.elems_per_page >= self.num_elems then return end
  443.     -- render up arrow if we're not at the top
  444.     dc:seek(0, 0):char(
  445.         self.top_elem == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR, self.fg, self.bg)
  446.     -- render scrollbar body
  447.     local starty = self.bar_offset + 1
  448.     local endy = self.bar_offset + self.bar_height
  449.     for y=1,dc.height-2 do
  450.         dc:seek(0, y)
  451.         if y >= starty and y <= endy then
  452.             dc:char(BAR_CHAR, self.fg)
  453.         else
  454.             dc:char(BAR_BG_CHAR, self.bg)
  455.         end
  456.     end
  457.     -- render down arrow if we're not at the bottom
  458.     local last_visible_el = self.top_elem + self.elems_per_page - 1
  459.     dc:seek(0, dc.height-1):char(
  460.         last_visible_el >= self.num_elems and NO_ARROW_CHAR or DOWN_ARROW_CHAR,
  461.         self.fg, self.bg)
  462.     if not self.on_scroll then return end
  463.     -- manage state for dragging and continuous scrolling
  464.     if self.is_dragging then
  465.         scrollbar_do_drag(self)
  466.     end
  467.     if df.global.enabler.mouse_lbut_down == 0 then
  468.         self.last_scroll_ms = 0
  469.         self.is_dragging = false
  470.         self.scroll_spec = nil
  471.         return
  472.     end
  473.     if self.last_scroll_ms == 0 then return end
  474.     local now = dfhack.getTickCount()
  475.     local delay = self.is_first_click and
  476.             SCROLL_INITIAL_DELAY_MS or SCROLL_DELAY_MS
  477.     if now - self.last_scroll_ms >= delay then
  478.         self.is_first_click = false
  479.         self.on_scroll(self.scroll_spec)
  480.         self.last_scroll_ms = now
  481.     end
  482. end
  483.  
  484. function Scrollbar:onInput(keys)
  485.     if not keys._MOUSE_L_DOWN or not self.on_scroll then return false end
  486.     local _,y = self:getMousePos()
  487.     if not y then return false end
  488.     local scroll_spec = nil
  489.     if y == 0 then scroll_spec = 'up_small'
  490.     elseif y == self.frame_body.height - 1 then scroll_spec = 'down_small'
  491.     elseif y <= self.bar_offset then scroll_spec = 'up_large'
  492.     elseif y > self.bar_offset + self.bar_height then scroll_spec = 'down_large'
  493.     else
  494.         self.is_dragging = y - self.bar_offset
  495.         return true
  496.     end
  497.     self.scroll_spec = scroll_spec
  498.     self.on_scroll(scroll_spec)
  499.     -- reset continuous scroll state
  500.     self.is_first_click = true
  501.     self.last_scroll_ms = dfhack.getTickCount()
  502.     return true
  503. end
  504.  
  505. -----------
  506. -- Label --
  507. -----------
  508.  
  509. function parse_label_text(obj)
  510.     local text = obj.text or {}
  511.     if type(text) ~= 'table' then
  512.         text = { text }
  513.     end
  514.     local curline = nil
  515.     local out = { }
  516.     local active = nil
  517.     local idtab = nil
  518.     for _,v in ipairs(text) do
  519.         local vv
  520.         if type(v) == 'string' then
  521.             vv = v:split(NEWLINE)
  522.         else
  523.             vv = { v }
  524.         end
  525.  
  526.         for i = 1,#vv do
  527.             local cv = vv[i]
  528.             if i > 1 then
  529.                 if not curline then
  530.                     table.insert(out, {})
  531.                 end
  532.                 curline = nil
  533.             end
  534.             if cv ~= '' then
  535.                 if not curline then
  536.                     curline = {}
  537.                     table.insert(out, curline)
  538.                 end
  539.  
  540.                 if type(cv) == 'string' then
  541.                     table.insert(curline, { text = cv })
  542.                 else
  543.                     table.insert(curline, cv)
  544.  
  545.                     if cv.on_activate then
  546.                         active = active or {}
  547.                         table.insert(active, cv)
  548.                     end
  549.  
  550.                     if cv.id then
  551.                         idtab = idtab or {}
  552.                         idtab[cv.id] = cv
  553.                     end
  554.                 end
  555.             end
  556.         end
  557.     end
  558.     obj.text_lines = out
  559.     obj.text_active = active
  560.     obj.text_ids = idtab
  561. end
  562.  
  563. local function is_disabled(token)
  564.     return (token.disabled ~= nil and getval(token.disabled)) or
  565.            (token.enabled ~= nil and not getval(token.enabled))
  566. end
  567.  
  568. function render_text(obj,dc,x0,y0,pen,dpen,disabled)
  569.     local width = 0
  570.     for iline = dc and obj.start_line_num or 1, #obj.text_lines do
  571.         local x, line = 0, obj.text_lines[iline]
  572.         if dc then
  573.             local offset = (obj.start_line_num or 1) - 1
  574.             local y = y0 + iline - offset - 1
  575.             -- skip text outside of the containing frame
  576.             if y > dc.height - 1 then break end
  577.             dc:seek(x+x0, y)
  578.         end
  579.         for _,token in ipairs(line) do
  580.             token.line = iline
  581.             token.x1 = x
  582.  
  583.             if token.gap then
  584.                 x = x + token.gap
  585.                 if dc then
  586.                     dc:advance(token.gap)
  587.                 end
  588.             end
  589.  
  590.             if token.tile then
  591.                 x = x + 1
  592.                 if dc then
  593.                     dc:char(nil, token.tile)
  594.                 end
  595.             end
  596.  
  597.             if token.text or token.key then
  598.                 local text = ''..(getval(token.text) or '')
  599.                 local keypen = dfhack.pen.parse(token.key_pen or COLOR_LIGHTGREEN)
  600.  
  601.                 if dc then
  602.                     local tpen = getval(token.pen)
  603.                     if disabled or is_disabled(token) then
  604.                         dc:pen(getval(token.dpen) or tpen or dpen)
  605.                         if keypen.fg ~= COLOR_BLACK then
  606.                             keypen.bold = false
  607.                         end
  608.                     else
  609.                         dc:pen(tpen or pen)
  610.                     end
  611.                 end
  612.  
  613.                 local width = getval(token.width)
  614.                 local padstr
  615.                 if width then
  616.                     x = x + width
  617.                     if #text > width then
  618.                         text = string.sub(text,1,width)
  619.                     else
  620.                         if token.pad_char then
  621.                             padstr = string.rep(token.pad_char,width-#text)
  622.                         end
  623.                         if dc and token.rjustify then
  624.                             if padstr then dc:string(padstr) else dc:advance(width-#text) end
  625.                         end
  626.                     end
  627.                 else
  628.                     x = x + #text
  629.                 end
  630.  
  631.                 if token.key then
  632.                     local keystr = gui.getKeyDisplay(token.key)
  633.                     local sep = token.key_sep or ''
  634.  
  635.                     x = x + #keystr
  636.  
  637.                     if sep:startswith('()') then
  638.                         if dc then
  639.                             dc:string(text)
  640.                             dc:string(' ('):string(keystr,keypen)
  641.                             dc:string(sep:sub(2))
  642.                         end
  643.                         x = x + 1 + #sep
  644.                     else
  645.                         if dc then
  646.                             dc:string(keystr,keypen):string(sep):string(text)
  647.                         end
  648.                         x = x + #sep
  649.                     end
  650.                 else
  651.                     if dc then
  652.                         dc:string(text)
  653.                     end
  654.                 end
  655.  
  656.                 if width and dc and not token.rjustify then
  657.                     if padstr then dc:string(padstr) else dc:advance(width-#text) end
  658.                 end
  659.             end
  660.  
  661.             token.x2 = x
  662.         end
  663.         width = math.max(width, x)
  664.     end
  665.     obj.text_width = width
  666. end
  667.  
  668. function check_text_keys(self, keys)
  669.     if self.text_active then
  670.         for _,item in ipairs(self.text_active) do
  671.             if item.key and keys[item.key] and not is_disabled(item) then
  672.                 item.on_activate()
  673.                 return true
  674.             end
  675.         end
  676.     end
  677. end
  678.  
  679. Label = defclass(Label, Widget)
  680.  
  681. Label.ATTRS{
  682.     text_pen = COLOR_WHITE,
  683.     text_dpen = COLOR_DARKGREY, -- disabled
  684.     text_hpen = DEFAULT_NIL, -- highlight - default is text_pen with reversed brightness
  685.     disabled = DEFAULT_NIL,
  686.     enabled = DEFAULT_NIL,
  687.     auto_height = true,
  688.     auto_width = false,
  689.     on_click = DEFAULT_NIL,
  690.     on_rclick = DEFAULT_NIL,
  691.     scroll_keys = STANDARDSCROLL,
  692. }
  693.  
  694. function Label:init(args)
  695.     self.scrollbar = Scrollbar{
  696.         frame={r=0},
  697.         on_scroll=self:callback('on_scrollbar')}
  698.  
  699.     self:addviews{self.scrollbar}
  700.  
  701.     -- use existing saved text if no explicit text was specified. this avoids
  702.     -- overwriting pre-formatted text that subclasses may have already set
  703.     self:setText(args.text or self.text)
  704.     if not self.text_hpen then
  705.         self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16
  706.     end
  707. end
  708.  
  709. local function update_label_scrollbar(label)
  710.     local body_height = label.frame_body and label.frame_body.height or 1
  711.     label.scrollbar:update(label.start_line_num, body_height,
  712.                            label:getTextHeight())
  713. end
  714.  
  715. function Label:setText(text)
  716.     self.start_line_num = 1
  717.     self.text = text
  718.     parse_label_text(self)
  719.  
  720.     if self.auto_height then
  721.         self.frame = self.frame or {}
  722.         self.frame.h = self:getTextHeight()
  723.     end
  724.  
  725.     update_label_scrollbar(self)
  726. end
  727.  
  728. function Label:preUpdateLayout()
  729.     if self.auto_width then
  730.         self.frame = self.frame or {}
  731.         self.frame.w = self:getTextWidth()
  732.     end
  733. end
  734.  
  735. function Label:postUpdateLayout()
  736.     update_label_scrollbar(self)
  737. end
  738.  
  739. function Label:itemById(id)
  740.     if self.text_ids then
  741.         return self.text_ids[id]
  742.     end
  743. end
  744.  
  745. function Label:getTextHeight()
  746.     return #self.text_lines
  747. end
  748.  
  749. function Label:getTextWidth()
  750.     render_text(self)
  751.     return self.text_width
  752. end
  753.  
  754. function Label:onRenderBody(dc)
  755.     local text_pen = self.text_pen
  756.     if self:getMousePos() and (self.on_click or self.on_rclick) then
  757.         text_pen = self.text_hpen
  758.     end
  759.     render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self))
  760. end
  761.  
  762. function Label:on_scrollbar(scroll_spec)
  763.     local v = 0
  764.     if scroll_spec == 'down_large' then
  765.         v = '+halfpage'
  766.     elseif scroll_spec == 'up_large' then
  767.         v = '-halfpage'
  768.     elseif scroll_spec == 'down_small' then
  769.         v = 1
  770.     elseif scroll_spec == 'up_small' then
  771.         v = -1
  772.     end
  773.  
  774.     self:scroll(v)
  775. end
  776.  
  777. function Label:scroll(nlines)
  778.     if not nlines then return end
  779.     if type(nlines) == 'string' then
  780.         if nlines == '+page' then
  781.             nlines = self.frame_body.height
  782.         elseif nlines == '-page' then
  783.             nlines = -self.frame_body.height
  784.         elseif nlines == '+halfpage' then
  785.             nlines = math.ceil(self.frame_body.height/2)
  786.         elseif nlines == '-halfpage' then
  787.             nlines = -math.ceil(self.frame_body.height/2)
  788.         else
  789.             error(('unhandled scroll keyword: "%s"'):format(nlines))
  790.         end
  791.     end
  792.     local n = self.start_line_num + nlines
  793.     n = math.min(n, self:getTextHeight() - self.frame_body.height + 1)
  794.     n = math.max(n, 1)
  795.     nlines = n - self.start_line_num
  796.     self.start_line_num = n
  797.     update_label_scrollbar(self)
  798.     return nlines
  799. end
  800.  
  801. function Label:onInput(keys)
  802.     if is_disabled(self) then return false end
  803.     if self:inputToSubviews(keys) then
  804.         return true
  805.     end
  806.     if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then
  807.         self:on_click()
  808.         return true
  809.     end
  810.     if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then
  811.         self:on_rclick()
  812.         return true
  813.     end
  814.     for k,v in pairs(self.scroll_keys) do
  815.         if keys[k] and 0 ~= self:scroll(v) then
  816.             return true
  817.         end
  818.     end
  819.     return check_text_keys(self, keys)
  820. end
  821.  
  822. ------------------
  823. -- WrappedLabel --
  824. ------------------
  825.  
  826. WrappedLabel = defclass(WrappedLabel, Label)
  827.  
  828. WrappedLabel.ATTRS{
  829.     text_to_wrap=DEFAULT_NIL,
  830.     indent=0,
  831. }
  832.  
  833. function WrappedLabel:getWrappedText(width)
  834.     -- 0 width can happen if the parent has 0 width
  835.     if not self.text_to_wrap or width <= 0 then return nil end
  836.     local text_to_wrap = getval(self.text_to_wrap)
  837.     if type(text_to_wrap) == 'table' then
  838.         text_to_wrap = table.concat(text_to_wrap, NEWLINE)
  839.     end
  840.     return text_to_wrap:wrap(width - self.indent)
  841. end
  842.  
  843. -- we can't set the text in init() since we may not yet have a frame that we
  844. -- can get wrapping bounds from.
  845. function WrappedLabel:postComputeFrame()
  846.     local wrapped_text = self:getWrappedText(self.frame_body.width-1)
  847.     if not wrapped_text then return end
  848.     local text = {}
  849.     for _,line in ipairs(wrapped_text:split(NEWLINE)) do
  850.         table.insert(text, {gap=self.indent, text=line})
  851.         -- a trailing newline will get ignored so we don't have to manually trim
  852.         table.insert(text, NEWLINE)
  853.     end
  854.     self:setText(text)
  855. end
  856.  
  857. ------------------
  858. -- TooltipLabel --
  859. ------------------
  860.  
  861. TooltipLabel = defclass(TooltipLabel, WrappedLabel)
  862.  
  863. TooltipLabel.ATTRS{
  864.     show_tooltip=DEFAULT_NIL,
  865.     indent=2,
  866.     text_pen=COLOR_GREY,
  867. }
  868.  
  869. function TooltipLabel:preUpdateLayout()
  870.     self.visible = getval(self.show_tooltip)
  871. end
  872.  
  873. -----------------
  874. -- HotkeyLabel --
  875. -----------------
  876.  
  877. HotkeyLabel = defclass(HotkeyLabel, Label)
  878.  
  879. HotkeyLabel.ATTRS{
  880.     key=DEFAULT_NIL,
  881.     key_sep=': ',
  882.     label=DEFAULT_NIL,
  883.     on_activate=DEFAULT_NIL,
  884. }
  885.  
  886. function HotkeyLabel:init()
  887.     self:setText{{key=self.key, key_sep=self.key_sep, text=self.label,
  888.                   on_activate=self.on_activate}}
  889. end
  890.  
  891. function HotkeyLabel:onInput(keys)
  892.     if HotkeyLabel.super.onInput(self, keys) then
  893.         return true
  894.     elseif keys._MOUSE_L and self:getMousePos() then
  895.         self.on_activate()
  896.         return true
  897.     end
  898. end
  899.  
  900. ----------------------
  901. -- CycleHotkeyLabel --
  902. ----------------------
  903.  
  904. CycleHotkeyLabel = defclass(CycleHotkeyLabel, Label)
  905.  
  906. CycleHotkeyLabel.ATTRS{
  907.     key=DEFAULT_NIL,
  908.     label=DEFAULT_NIL,
  909.     label_width=DEFAULT_NIL,
  910.     options=DEFAULT_NIL,
  911.     initial_option=1,
  912.     on_change=DEFAULT_NIL,
  913. }
  914.  
  915. function CycleHotkeyLabel:init()
  916.     -- initialize option_idx
  917.     for i in ipairs(self.options) do
  918.         if self.initial_option == self:getOptionValue(i) then
  919.             self.option_idx = i
  920.             break
  921.         end
  922.     end
  923.     if not self.option_idx then
  924.         if self.options[self.initial_option] then
  925.             self.option_idx = self.initial_option
  926.         end
  927.     end
  928.     if not self.option_idx then
  929.         error(('cannot find option with value or index: "%s"')
  930.               :format(self.initial_option))
  931.     end
  932.  
  933.     self:setText{
  934.         {key=self.key, key_sep=': ', text=self.label, width=self.label_width,
  935.          on_activate=self:callback('cycle')},
  936.         '  ',
  937.         {text=self:callback('getOptionLabel')},
  938.     }
  939. end
  940.  
  941. function CycleHotkeyLabel:cycle()
  942.     local old_option_idx = self.option_idx
  943.     if self.option_idx == #self.options then
  944.         self.option_idx = 1
  945.     else
  946.         self.option_idx = self.option_idx + 1
  947.     end
  948.     if self.on_change then
  949.         self.on_change(self:getOptionValue(),
  950.                        self:getOptionValue(old_option_idx))
  951.     end
  952. end
  953.  
  954. function CycleHotkeyLabel:getOptionLabel(option_idx)
  955.     option_idx = option_idx or self.option_idx
  956.     local option = self.options[option_idx]
  957.     if type(option) == 'table' then
  958.         return option.label
  959.     end
  960.     return option
  961. end
  962.  
  963. function CycleHotkeyLabel:getOptionValue(option_idx)
  964.     option_idx = option_idx or self.option_idx
  965.     local option = self.options[option_idx]
  966.     if type(option) == 'table' then
  967.         return option.value
  968.     end
  969.     return option
  970. end
  971.  
  972. function CycleHotkeyLabel:onInput(keys)
  973.     if CycleHotkeyLabel.super.onInput(self, keys) then
  974.         return true
  975.     elseif keys._MOUSE_L and self:getMousePos() then
  976.         self:cycle()
  977.         return true
  978.     end
  979. end
  980.  
  981. -----------------------
  982. -- ToggleHotkeyLabel --
  983. -----------------------
  984.  
  985. ToggleHotkeyLabel = defclass(ToggleHotkeyLabel, CycleHotkeyLabel)
  986. ToggleHotkeyLabel.ATTRS{
  987.     options={{label='On', value=true},
  988.              {label='Off', value=false}},
  989. }
  990.  
  991. ----------
  992. -- List --
  993. ----------
  994.  
  995. List = defclass(List, Widget)
  996.  
  997. List.ATTRS{
  998.     text_pen = COLOR_CYAN,
  999.     cursor_pen = COLOR_LIGHTCYAN,
  1000.     inactive_pen = DEFAULT_NIL,
  1001.     on_select = DEFAULT_NIL,
  1002.     on_submit = DEFAULT_NIL,
  1003.     on_submit2 = DEFAULT_NIL,
  1004.     row_height = 1,
  1005.     scroll_keys = STANDARDSCROLL,
  1006.     icon_width = DEFAULT_NIL,
  1007. }
  1008.  
  1009. function List:init(info)
  1010.     self.page_top = 1
  1011.     self.page_size = 1
  1012.     self.scrollbar = Scrollbar{
  1013.         frame={r=0},
  1014.         on_scroll=self:callback('on_scrollbar')}
  1015.  
  1016.     self:addviews{self.scrollbar}
  1017.  
  1018.     if info.choices then
  1019.         self:setChoices(info.choices, info.selected)
  1020.     else
  1021.         self.choices = {}
  1022.         self.selected = 1
  1023.     end
  1024. end
  1025.  
  1026. function List:setChoices(choices, selected)
  1027.     self.choices = {}
  1028.  
  1029.     for i,v in ipairs(choices or {}) do
  1030.         local l = utils.clone(v);
  1031.         if type(v) ~= 'table' then
  1032.             l = { text = v }
  1033.         else
  1034.             l.text = v.text or v.caption or v[1]
  1035.         end
  1036.         parse_label_text(l)
  1037.         self.choices[i] = l
  1038.     end
  1039.  
  1040.     self:setSelected(selected)
  1041. end
  1042.  
  1043. function List:setSelected(selected)
  1044.     self.selected = selected or self.selected or 1
  1045.     self:moveCursor(0, true)
  1046.     return self.selected
  1047. end
  1048.  
  1049. function List:getChoices()
  1050.     return self.choices
  1051. end
  1052.  
  1053. function List:getSelected()
  1054.     if #self.choices > 0 then
  1055.         return self.selected, self.choices[self.selected]
  1056.     end
  1057. end
  1058.  
  1059. function List:getContentWidth()
  1060.     local width = 0
  1061.     for i,v in ipairs(self.choices) do
  1062.         render_text(v)
  1063.         local roww = v.text_width
  1064.         if v.key then
  1065.             roww = roww + 3 + #gui.getKeyDisplay(v.key)
  1066.         end
  1067.         width = math.max(width, roww)
  1068.     end
  1069.     return width + (self.icon_width or 0)
  1070. end
  1071.  
  1072. function List:getContentHeight()
  1073.     return #self.choices * self.row_height
  1074. end
  1075.  
  1076. function List:postComputeFrame(body)
  1077.     self.page_size = math.max(1, math.floor(body.height / self.row_height))
  1078.     self:moveCursor(0)
  1079. end
  1080.  
  1081. local function update_list_scrollbar(list)
  1082.     list.scrollbar:update(list.page_top, list.page_size, #list.choices)
  1083. end
  1084.  
  1085. function List:postUpdateLayout()
  1086.     update_list_scrollbar(self)
  1087. end
  1088.  
  1089. function List:moveCursor(delta, force_cb)
  1090.     local cnt = #self.choices
  1091.  
  1092.     if cnt < 1 then
  1093.         self.page_top = 1
  1094.         self.selected = 1
  1095.         update_list_scrollbar(self)
  1096.         if force_cb and self.on_select then
  1097.             self.on_select(nil,nil)
  1098.         end
  1099.         return
  1100.     end
  1101.  
  1102.     local off = self.selected+delta-1
  1103.     local ds = math.abs(delta)
  1104.  
  1105.     if ds > 1 then
  1106.         if off >= cnt+ds-1 then
  1107.             off = 0
  1108.         else
  1109.             off = math.min(cnt-1, off)
  1110.         end
  1111.         if off <= -ds then
  1112.             off = cnt-1
  1113.         else
  1114.             off = math.max(0, off)
  1115.         end
  1116.     end
  1117.  
  1118.     local buffer = 1 + math.min(4, math.floor(self.page_size/10))
  1119.  
  1120.     self.selected = 1 + off % cnt
  1121.     if (self.selected - buffer) < self.page_top then
  1122.         self.page_top = math.max(1, self.selected - buffer)
  1123.     elseif (self.selected + buffer + 1) > (self.page_top + self.page_size) then
  1124.         local max_page_top = cnt - self.page_size + 1
  1125.         self.page_top = math.max(1,
  1126.             math.min(max_page_top, self.selected - self.page_size + buffer + 1))
  1127.     end
  1128.     update_list_scrollbar(self)
  1129.  
  1130.     if (force_cb or delta ~= 0) and self.on_select then
  1131.         self.on_select(self:getSelected())
  1132.     end
  1133. end
  1134.  
  1135. function List:on_scrollbar(scroll_spec)
  1136.     local v = 0
  1137.     if scroll_spec == 'down_large' then
  1138.         v = math.ceil(self.page_size / 2)
  1139.     elseif scroll_spec == 'up_large' then
  1140.         v = -math.ceil(self.page_size / 2)
  1141.     elseif scroll_spec == 'down_small' then
  1142.         v = 1
  1143.     elseif scroll_spec == 'up_small' then
  1144.         v = -1
  1145.     end
  1146.  
  1147.     local max_page_top = math.max(1, #self.choices - self.page_size + 1)
  1148.     self.page_top = math.max(1, math.min(max_page_top, self.page_top + v))
  1149.     update_list_scrollbar(self)
  1150. end
  1151.  
  1152. function List:onRenderBody(dc)
  1153.     local choices = self.choices
  1154.     local top = self.page_top
  1155.     local iend = math.min(#choices, top+self.page_size-1)
  1156.     local iw = self.icon_width
  1157.  
  1158.     local function paint_icon(icon, obj)
  1159.         if type(icon) ~= 'string' then
  1160.             dc:char(nil,icon)
  1161.         else
  1162.             if current then
  1163.                 dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen)
  1164.             else
  1165.                 dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen)
  1166.             end
  1167.         end
  1168.     end
  1169.  
  1170.     for i = top,iend do
  1171.         local obj = choices[i]
  1172.         local current = (i == self.selected)
  1173.         local cur_pen = self.cursor_pen
  1174.         local cur_dpen = self.text_pen
  1175.         local active_pen = current and cur_pen or cur_dpen
  1176.  
  1177.         if not self.active then
  1178.             cur_pen = self.inactive_pen or self.cursor_pen
  1179.         end
  1180.  
  1181.         local y = (i - top)*self.row_height
  1182.         local icon = getval(obj.icon)
  1183.  
  1184.         if iw and icon then
  1185.             dc:seek(0, y):pen(active_pen)
  1186.             paint_icon(icon, obj)
  1187.         end
  1188.  
  1189.         render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current)
  1190.  
  1191.         local ip = dc.width
  1192.  
  1193.         if obj.key then
  1194.             local keystr = gui.getKeyDisplay(obj.key)
  1195.             ip = ip-2-#keystr
  1196.             dc:seek(ip,y):pen(self.text_pen)
  1197.             dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')')
  1198.         end
  1199.  
  1200.         if icon and not iw then
  1201.             dc:seek(ip-1,y):pen(active_pen)
  1202.             paint_icon(icon, obj)
  1203.         end
  1204.     end
  1205. end
  1206.  
  1207. function List:submit()
  1208.     if self.on_submit and #self.choices > 0 then
  1209.         self.on_submit(self:getSelected())
  1210.     end
  1211. end
  1212.  
  1213. function List:submit2()
  1214.     if self.on_submit2 and #self.choices > 0 then
  1215.         self.on_submit2(self:getSelected())
  1216.     end
  1217. end
  1218.  
  1219. function List:onInput(keys)
  1220.     if self:inputToSubviews(keys) then
  1221.         return true
  1222.     end
  1223.     if self.on_submit and keys.SELECT then
  1224.         self:submit()
  1225.         return true
  1226.     elseif self.on_submit2 and keys.SEC_SELECT then
  1227.         self:submit2()
  1228.         return true
  1229.     elseif keys._MOUSE_L then
  1230.         local _, mouse_y = self:getMousePos()
  1231.         if mouse_y and #self.choices > 0 and
  1232.                 mouse_y < (#self.choices-self.page_top+1) * self.row_height then
  1233.             local idx = self.page_top + math.floor(mouse_y/self.row_height)
  1234.             self:setSelected(idx)
  1235.             self:submit()
  1236.             return true
  1237.         end
  1238.     else
  1239.         for k,v in pairs(self.scroll_keys) do
  1240.             if keys[k] then
  1241.                 if v == '+page' then
  1242.                     v = self.page_size
  1243.                 elseif v == '-page' then
  1244.                     v = -self.page_size
  1245.                 end
  1246.  
  1247.                 self:moveCursor(v)
  1248.                 return true
  1249.             end
  1250.         end
  1251.  
  1252.         for i,v in ipairs(self.choices) do
  1253.             if v.key and keys[v.key] then
  1254.                 self:setSelected(i)
  1255.                 self:submit()
  1256.                 return true
  1257.             end
  1258.         end
  1259.  
  1260.         local current = self.choices[self.selected]
  1261.         if current then
  1262.             return check_text_keys(current, keys)
  1263.         end
  1264.     end
  1265. end
  1266.  
  1267. -------------------
  1268. -- Filtered List --
  1269. -------------------
  1270.  
  1271. FilteredList = defclass(FilteredList, Widget)
  1272.  
  1273. FilteredList.ATTRS {
  1274.     edit_below = false,
  1275.     edit_key = DEFAULT_NIL,
  1276.     edit_ignore_keys = DEFAULT_NIL,
  1277. }
  1278.  
  1279. function FilteredList:init(info)
  1280.     self.edit = EditField{
  1281.         text_pen = info.edit_pen or info.cursor_pen,
  1282.         frame = { l = info.icon_width, t = 0, h = 1 },
  1283.         on_change = self:callback('onFilterChange'),
  1284.         on_char = self:callback('onFilterChar'),
  1285.         key = self.edit_key,
  1286.         ignore_keys = self.edit_ignore_keys,
  1287.     }
  1288.     self.list = List{
  1289.         frame = { t = 2 },
  1290.         text_pen = info.text_pen,
  1291.         cursor_pen = info.cursor_pen,
  1292.         inactive_pen = info.inactive_pen,
  1293.         icon_pen = info.icon_pen,
  1294.         row_height = info.row_height,
  1295.         scroll_keys = info.scroll_keys,
  1296.         icon_width = info.icon_width,
  1297.     }
  1298.     if self.edit_below then
  1299.         self.edit.frame = { l = info.icon_width, b = 0, h = 1 }
  1300.         self.list.frame = { t = 0, b = 2 }
  1301.     end
  1302.     if info.on_select then
  1303.         self.list.on_select = function()
  1304.             return info.on_select(self:getSelected())
  1305.         end
  1306.     end
  1307.     if info.on_submit then
  1308.         self.list.on_submit = function()
  1309.             return info.on_submit(self:getSelected())
  1310.         end
  1311.     end
  1312.     if info.on_submit2 then
  1313.         self.list.on_submit2 = function()
  1314.             return info.on_submit2(self:getSelected())
  1315.         end
  1316.     end
  1317.     self.not_found = Label{
  1318.         visible = true,
  1319.         text = info.not_found_label or 'No matches',
  1320.         text_pen = COLOR_LIGHTRED,
  1321.         frame = { l = info.icon_width, t = self.list.frame.t },
  1322.     }
  1323.     self:addviews{ self.edit, self.list, self.not_found }
  1324.     if info.choices then
  1325.         self:setChoices(info.choices, info.selected)
  1326.     else
  1327.         self.choices = {}
  1328.     end
  1329. end
  1330.  
  1331. function FilteredList:getChoices()
  1332.     return self.choices
  1333. end
  1334.  
  1335. function FilteredList:getVisibleChoices()
  1336.     return self.list.choices
  1337. end
  1338.  
  1339. function FilteredList:setChoices(choices, pos)
  1340.     choices = choices or {}
  1341.     self.edit:setText('')
  1342.     self.list:setChoices(choices, pos)
  1343.     self.choices = self.list.choices
  1344.     self.not_found.visible = (#choices == 0)
  1345. end
  1346.  
  1347. function FilteredList:submit()
  1348.     return self.list:submit()
  1349. end
  1350.  
  1351. function FilteredList:submit2()
  1352.     return self.list:submit2()
  1353. end
  1354.  
  1355. function FilteredList:canSubmit()
  1356.     return not self.not_found.visible
  1357. end
  1358.  
  1359. function FilteredList:getSelected()
  1360.     local i,v = self.list:getSelected()
  1361.     if i then
  1362.         return map_opttab(self.choice_index, i), v
  1363.     end
  1364. end
  1365.  
  1366. function FilteredList:getContentWidth()
  1367.     return self.list:getContentWidth()
  1368. end
  1369.  
  1370. function FilteredList:getContentHeight()
  1371.     return self.list:getContentHeight() + 2
  1372. end
  1373.  
  1374. function FilteredList:getFilter()
  1375.     return self.edit.text, self.list.choices
  1376. end
  1377.  
  1378. function FilteredList:setFilter(filter, pos)
  1379.     local choices = self.choices
  1380.     local cidx = nil
  1381.  
  1382.     filter = filter or ''
  1383.     if filter ~= self.edit.text then
  1384.         self.edit:setText(filter)
  1385.     end
  1386.  
  1387.     if filter ~= '' then
  1388.         local tokens = filter:split()
  1389.         local ipos = pos
  1390.  
  1391.         choices = {}
  1392.         cidx = {}
  1393.         pos = nil
  1394.  
  1395.         for i,v in ipairs(self.choices) do
  1396.             local ok = true
  1397.             local search_key = v.search_key or v.text
  1398.             for _,key in ipairs(tokens) do
  1399.                 key = key:escape_pattern()
  1400.                 -- start matches at non-space or non-punctuation. this allows
  1401.                 -- punctuation itself to be matched if that is useful (e.g.
  1402.                 -- filenames or parameter names)
  1403.                 if key ~= '' and
  1404.                         not search_key:match('%f[^%p\x00]'..key) and
  1405.                         not search_key:match('%f[^%s\x00]'..key) then
  1406.                     ok = false
  1407.                     break
  1408.                 end
  1409.             end
  1410.             if ok then
  1411.                 table.insert(choices, v)
  1412.                 cidx[#choices] = i
  1413.                 if ipos == i then
  1414.                     pos = #choices
  1415.                 end
  1416.             end
  1417.         end
  1418.     end
  1419.  
  1420.     self.choice_index = cidx
  1421.     self.list:setChoices(choices, pos)
  1422.     self.not_found.visible = (#choices == 0)
  1423. end
  1424.  
  1425. function FilteredList:onFilterChange(text)
  1426.     self:setFilter(text)
  1427. end
  1428.  
  1429. function FilteredList:onFilterChar(char, text)
  1430.     if char == ' ' then
  1431.         return string.match(text, '%S$')
  1432.     end
  1433.     return true
  1434. end
  1435.  
  1436. return _ENV
  1437.  
Add Comment
Please, Sign In to add comment