forked from DFHack/dfhack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlist.lua
More file actions
388 lines (333 loc) · 10.7 KB
/
Copy pathlist.lua
File metadata and controls
388 lines (333 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
local gui = require('gui')
local utils = require('utils')
local common = require('gui.widgets.common')
local Widget = require('gui.widgets.widget')
local Scrollbar = require('gui.widgets.scrollbar')
local Label = require('gui.widgets.labels.label')
local getval = utils.getval
local to_pen = dfhack.pen.parse
----------
-- List --
----------
---@class widgets.ListChoice
---@field text string|widgets.LabelToken[]
---@field key string
---@field search_key? string
---@field icon? string|dfhack.pen|fun(): string|dfhack.pen
---@field icon_pen? dfhack.pen
---@class widgets.List.attrs: widgets.Widget.attrs
---@field choices widgets.ListChoice[]
---@field selected integer
---@field text_pen dfhack.color|dfhack.pen
---@field text_hpen? dfhack.color|dfhack.pen
---@field cursor_pen dfhack.color|dfhack.pen
---@field inactive_pen? dfhack.color|dfhack.pen
---@field icon_pen? dfhack.color|dfhack.pen
---@field on_select? function
---@field on_submit? function
---@field on_submit2? function
---@field on_double_click? function
---@field on_double_click2? function
---@field row_height integer
---@field scroll_keys table<string, string|integer>
---@field icon_width? integer
---@field page_top integer
---@field page_size integer
---@field scrollbar widgets.Scrollbar
---@field last_select_click_ms integer
---@class widgets.List.attrs.partial: widgets.List.attrs
---@class widgets.List: widgets.Widget, widgets.List.attrs
---@field super widgets.Widget
---@field ATTRS widgets.List.attrs|fun(attributes: widgets.List.attrs.partial)
---@overload fun(init_table: widgets.List.attrs.partial): self
List = defclass(List, Widget)
List.ATTRS{
text_pen = COLOR_CYAN,
text_hpen = DEFAULT_NIL, -- hover color, defaults to inverting the FG/BG pens for each text object
cursor_pen = COLOR_LIGHTCYAN,
inactive_pen = DEFAULT_NIL,
icon_pen = DEFAULT_NIL,
on_select = DEFAULT_NIL,
on_submit = DEFAULT_NIL,
on_submit2 = DEFAULT_NIL,
on_double_click = DEFAULT_NIL,
on_double_click2 = DEFAULT_NIL,
row_height = 1,
scroll_keys = common.STANDARDSCROLL,
icon_width = DEFAULT_NIL,
}
---@param self widgets.List
---@param info widgets.List.attrs.partial
function List:init(info)
self.page_top = 1
self.page_size = 1
self.scrollbar = Scrollbar{
frame={r=0},
on_scroll=self:callback('on_scrollbar')}
self:addviews{self.scrollbar}
if info.choices then
self:setChoices(info.choices, info.selected)
else
self.choices = {}
self.selected = 1
end
self.last_select_click_ms = 0 -- used to track double-clicking on an item
end
function List:setChoices(choices, selected)
self.choices = {}
for i,v in ipairs(choices or {}) do
local l = utils.clone(v);
if type(v) ~= 'table' then
l = { text = v }
else
l.text = v.text or v.caption or v[1]
end
Label.parse_label_text(l)
self.choices[i] = l
end
self:setSelected(selected)
-- Check if page_top needs to be adjusted
if #self.choices - self.page_size < 0 then
self.page_top = 1
elseif self.page_top > #self.choices - self.page_size + 1 then
self.page_top = #self.choices - self.page_size + 1
end
end
function List:setSelected(selected)
self.selected = selected or self.selected or 1
self:moveCursor(0, true)
return self.selected
end
function List:getChoices()
return self.choices
end
function List:getSelected()
if #self.choices > 0 then
return self.selected, self.choices[self.selected]
end
end
function List:getContentWidth()
local width = 0
for i,v in ipairs(self.choices) do
Label.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
return width + (self.icon_width or 0)
end
function List:getContentHeight()
return #self.choices * self.row_height
end
local function update_list_scrollbar(list)
list.scrollbar:update(list.page_top, list.page_size, #list.choices)
end
function List:postComputeFrame(body)
local row_count = body.height // self.row_height
self.page_size = math.max(1, row_count)
local num_choices = #self.choices
if num_choices == 0 then
self.page_top = 1
update_list_scrollbar(self)
return
end
if self.page_top > num_choices - self.page_size + 1 then
self.page_top = math.max(1, num_choices - self.page_size + 1)
end
update_list_scrollbar(self)
end
function List:postUpdateLayout()
update_list_scrollbar(self)
end
function List:moveCursor(delta, force_cb)
local cnt = #self.choices
if cnt < 1 then
self.page_top = 1
self.selected = 1
update_list_scrollbar(self)
if force_cb and self.on_select then
self.on_select(nil,nil)
end
return
end
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
local buffer = 1 + math.min(4, math.floor(self.page_size/10))
self.selected = 1 + off % cnt
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)
if (force_cb or delta ~= 0) and self.on_select then
self.on_select(self:getSelected())
end
end
function List:on_scrollbar(scroll_spec)
local v = 0
if tonumber(scroll_spec) then
v = scroll_spec - self.page_top
elseif scroll_spec == 'down_large' then
v = math.ceil(self.page_size / 2)
elseif scroll_spec == 'up_large' then
v = -math.ceil(self.page_size / 2)
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
function List:onRenderBody(dc)
local choices = self.choices
local top = self.page_top
local iend = math.min(#choices, top+self.page_size-1)
local iw = self.icon_width
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
local hoveridx = self:getIdxUnderMouse()
for i = top,iend do
local obj = choices[i]
local current = (i == self.selected)
local hovered = (i == hoveridx)
-- cur_pen and cur_dpen can't be integers or background colors get
-- messed up in render_text for subsequent renders
local cur_pen = to_pen(self.cursor_pen)
local cur_dpen = to_pen(self.text_pen)
local active_pen = (current and cur_pen or cur_dpen)
if not getval(self.active) then
cur_pen = self.inactive_pen or self.cursor_pen
end
local y = (i - top)*self.row_height
local icon = getval(obj.icon)
if iw and icon then
dc:seek(0, y):pen(active_pen)
paint_icon(icon, obj)
end
Label.render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered)
local ip = dc.width
if obj.key then
local keystr = gui.getKeyDisplay(obj.key)
ip = ip-3-#keystr
dc:seek(ip,y):pen(self.text_pen)
dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')')
end
if icon and not iw then
dc:seek(ip-1,y):pen(active_pen)
paint_icon(icon, obj)
end
end
end
function List:getIdxUnderMouse()
if self.scrollbar:getMousePos() then return end
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
function List:submit()
if self.on_submit and #self.choices > 0 then
self.on_submit(self:getSelected())
return true
end
end
function List:submit2()
if self.on_submit2 and #self.choices > 0 then
self.on_submit2(self:getSelected())
return true
end
end
function List:double_click()
if #self.choices == 0 then return end
local cb = dfhack.internal.getModifiers().shift and
self.on_double_click2 or self.on_double_click
if cb then
cb(self:getSelected())
return true
end
end
function List:onInput(keys)
if self:inputToSubviews(keys) then
return true
end
if keys.SELECT then
return self:submit()
elseif keys.SELECT_ALL then
return self:submit2()
elseif keys._MOUSE_L then
local idx = self:getIdxUnderMouse()
if idx then
local now_ms = dfhack.getTickCount()
if idx ~= self:getSelected() then
self.last_select_click_ms = now_ms
else
if now_ms - self.last_select_click_ms <= common.DOUBLE_CLICK_MS then
self.last_select_click_ms = 0
if self:double_click() then return true end
else
self.last_select_click_ms = now_ms
end
end
self:setSelected(idx)
if dfhack.internal.getModifiers().shift then
self:submit2()
else
self:submit()
end
return true
end
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)
self:submit()
return true
end
end
local current = self.choices[self.selected]
if current then
return Label.check_text_keys(current, keys)
end
end
end
return List