SHOW:
|
|
- or go back to the newest paste.
1 | local helptext = [=[ | |
2 | indicator_screen | |
3 | ================ | |
4 | Module containing getScreen & isWideView functions, to be | |
5 | called with dfhack.script_environment(gui/indicator_screen) | |
6 | - | version 1.1 |
6 | + | version 1.2 |
7 | getScreen arguments: | |
8 | 1: A table with 1 to N tables ([] values being optional) of | |
9 | {text[, color][,onclick ][, onrclick][, notEndOfLine] | |
10 | [, onhovertext][, onhoverfunction]}, | |
11 | where text is text or function that returns text, color is | |
12 | number/pen or function that returns it, onclick and onrclick | |
13 | are functions to be called when the text is (right-)clicked, | |
14 | notEndOfLine being true continues next text on same line, | |
15 | onhovertext being a table with text and color used on hover, | |
16 | and onhoverfunction is function called on hover. Table can | |
17 | be changed after creation, and color/onclick/onrclick | |
18 | /onhoverfunction can also be applied to whole table. | |
19 | ||
20 | 2: Optional table that optionally contains x, y, width and | |
21 | height for text display. Places the text by upper left | |
22 | corner at x,y. Calculates height and width from 1 if not set. | |
23 | ||
24 | 3: Optional table like the above for frame boundary. | |
25 | If width or height are not set, uses DF's height and width. | |
26 | If left out, then frame boundary is not drawn. | |
27 | ||
28 | Returns a gui.FramedScreen element with additional functions: | |
29 | adjustDims(affectsText,x,y,w,h) - meant for resizing | |
30 | onHelp() - dismisses the screen | |
31 | removeFromView([dissmiss_when]) - [dismisses in n ticks] | |
32 | inheritBindings() | |
33 | clearBindings() | |
34 | onDestroy() | |
35 | onGetSelectedJob() | |
36 | onGetSelectedUnit() | |
37 | onInput(keys) | |
38 | onRenderBody() | |
39 | onRenderFrame() | |
40 | onResize() | |
41 | onShow() | |
42 | setRects(textrect, framerect) | |
43 | ||
44 | And public values affecting behaviour: | |
45 | signature: false will not paint, nil will use light gray | |
46 | signature_text: Text used as signature if there's no frame | |
47 | binding_inherit: valid values "hook", "keybinding", "none". | |
48 | dismiss_on_zoom: nil will dismiss parent and adopt grandpa | |
49 | dismiss_ordered: used to dismiss screen on viewscreen change. | |
50 | nonblinky_dismiss: default evaluates by fps/gfps>1+#screens. | |
51 | onStateChangeKey: dfhack.onStateChange function key for ^. ]=] | |
52 | ||
53 | indicator_screen_version = 1.2 | |
54 | ||
55 | local gui = require 'gui' | |
56 | ||
57 | function isWideView() | |
58 | -- Tells if one has tabbed the view mode to wide or not with true or false. | |
59 | -- In other cases, such as not even thin, returns nil. | |
60 | local map_width, menu_width | |
61 | if("number"==type(df.global.ui_menu_width)) then | |
62 | map_width = df.global.ui_area_map_width | |
63 | menu_width = df.global.ui_menu_width | |
64 | elseif(#df.global.ui_menu_width == 2) then | |
65 | map_width = df.global.ui_menu_width[1] | |
66 | menu_width = df.global.ui_menu_width[0] | |
67 | else | |
68 | print("gui/indicator-screen can't handle this menu width configuration") | |
69 | return true | |
70 | end | |
71 | if map_width == 3 and menu_width == 1 then | |
72 | return true | |
73 | elseif map_width == 2 and | |
74 | (menu_width == 1 or | |
75 | (--[==[dfhack.screen.readTile((df.global.gps.dimx-56),2).ch == 219 and--]==]-- | |
76 | -- To check the case where menu should be gone but active sidebar keeps it visible | |
77 | -- Deprecated: Introduces lag. | |
78 | (not dfhack.gui.getCurFocus():find("dwarfmode/Default")) and | |
79 | -- To prevent failing on Trade depots in NE corner, though a rogue screen could override this. | |
80 | menu_width == 2) | |
81 | ) --or ( | |
82 | --(not dfhack.gui.getCurFocus():find("dwarfmode/Default")) | |
83 | --and map_width == 3 and menu_width == 3 ) | |
84 | then | |
85 | return true | |
86 | elseif (map_width == 3 and menu_width == 3 and dfhack.gui.getCurFocus():find("dwarfmode/Default") ) or ( | |
87 | map_width == 2 and menu_width == 2 ) then | |
88 | return nil | |
89 | else | |
90 | return false | |
91 | end | |
92 | end | |
93 | ||
94 | ||
95 | function getScreen(dynamic_text_table, simpleTextRect, simpleFrameRect) | |
96 | --[[ :dynamic_text_table: a {} with 1..n iterated integer keys | |
97 | each points to {text = valueA, color = valueB} | |
98 | values can be functions | |
99 | :simpleTextRect: an optional {} with optional x, y, width, height number values | |
100 | if not set, uses window dimensions | |
101 | :simpleFrameRect: like simpleTextRect | |
102 | if not set, only places signature on closest vanilla frame border. | |
103 | ||
104 | Returns not yet shown screen | |
105 | ]]-- | |
106 | ||
107 | function getLongestLength (ltable, lkey) | |
108 | -- Single-Utility function, returns longest length of a given key or notEndOfLine keychain in a table. | |
109 | -- Runs functions once to get the length of their answers. | |
110 | local len,clen = 0,0 | |
111 | local twostring=tostring --function outside iterator | |
112 | for i=1, #ltable do | |
113 | if type(ltable[i][lkey]) == "function" then | |
114 | clen = clen + #((ltable[i][lkey])()) | |
115 | ||
116 | else | |
117 | clen = clen + #(twostring(ltable[i][lkey])) --convert to string, in case it is a number | |
118 | end | |
119 | if ltable[i].notEndOfLine and i ~= #ltable then | |
120 | --doNothing, unless it is the last element | |
121 | elseif clen > len then | |
122 | len = clen | |
123 | clen = 0 | |
124 | else | |
125 | clen = 0 | |
126 | end | |
127 | end | |
128 | return len | |
129 | end | |
130 | ||
131 | function getHeight(ltable) | |
132 | --single-use utility function to get height of text used. | |
133 | local height = #ltable | |
134 | for i=1, #ltable do | |
135 | if i~= #ltable and ltable[i].notEndOfLine then | |
136 | height = height - 1 | |
137 | end | |
138 | end | |
139 | return height | |
140 | end | |
141 | ||
142 | simpleTextRect = simpleTextRect or {} | |
143 | --In case it is not specified, as all methods refer to a table. | |
144 | ||
145 | local screen = gui.FramedScreen{ | |
146 | frame_style = gui.BOUNDARY_FRAME, -- not necessary, but indicatoror will default anyway | |
147 | --frame_title = '', -- Adds clutter and is not useful for most inline indicators. Can be set later | |
148 | frame_width = simpleTextRect.width or getLongestLength(dynamic_text_table, "text"), | |
149 | --function, as lenghts shorter clip it away, while longer ones create unnecesary black space | |
150 | frame_height = simpleTextRect.height or getHeight(dynamic_text_table), | |
151 | frame_inset = simpleFrameRect and 1 or -1 | |
152 | } | |
153 | ||
154 | simpleTextRect.width = simpleTextRect.width or screen.frame_body.width | |
155 | simpleTextRect.height = simpleTextRect.height or screen.frame_body.height | |
156 | ||
157 | local unused, nr = dfhack.gui.getCurFocus():gsub("dfhack","hack") | |
158 | screen.nonblinky_dismiss = df.global.enabler.fps_per_gfps < (2+(nr or 0)) and true or false | |
159 | unused, nr = nil, nil | |
160 | --Issue nbA: If one creates multiple screens at once and then later shows them at once, may blink. | |
161 | --But if one sets this value on show, then user will be unable to modify it if desired, | |
162 | --as the relevant necessary function to make it work is also called then and only then. | |
163 | --Issue nbB: Inaccurate if fps isn't capped (possible once this thing can unpause) | |
164 | --Issue nbC: if set to false, .dismiss_ordered does nothing. | |
165 | ||
166 | screen.signature = true | |
167 | -- only affects things if inset is -1 | |
168 | -- other options being nil (light gray on light gray) and false(not painted) | |
169 | ||
170 | screen.binding_inherit = "hook" | |
171 | --[[ Controls if and how the created screen will react to keybindings active in the context it is called | |
172 | Purpose is to provide better interaction with fourth party plugins and scripts | |
173 | ||
174 | Possible options are hook, hook_bottom, keybinding, none=nil=false | |
175 | ||
176 | none: doesn't look at the bindings set to viewscreen beneath, though focus path is set according to it. | |
177 | ||
178 | keybinding: Asks keybinding for all keybindings, and then adds binding in parent viewscreen to its context | |
179 | for as long as the screen is up. Doesn't always work even when it could, for scripts often check | |
180 | context they're called in before running. | |
181 | ||
182 | hook: Asks keybinding for all keybindings, and when it gets a combination applicable in parent screen removes | |
183 | itself as child from parent screen, calls the command with dfhack, waits a frame, and readds itself. | |
184 | After 1 frame, adds this screen back on top of topmost viewscreen. | |
185 | ]]-- | |
186 | ||
187 | screen.dismiss_on_zoom = true | |
188 | -- Controls whether screen is kept or dismissed on zooming or ascending (with Escape) out of a menu. | |
189 | -- Default is true, just as new vanilla screens are placed on top of created screens. | |
190 | -- False would mimic how binding_inherit hook results in this screen being placed on top of keybinded screen. | |
191 | function createRect(inputRect) | |
192 | --Takes nil or partial or complete table of x, y, width, height values | |
193 | -- negative x,y will be wrapped | |
194 | --Returns table with all location values screen uses. | |
195 | local simpleRect = inputRect or {} | |
196 | --Either use passed simple rectangle or create new one as big as screen. | |
197 | local newRect = {} | |
198 | newRect.width = simpleRect.width or df.global.gps.dimx | |
199 | newRect.height = simpleRect.height or df.global.gps.dimy | |
200 | --if values are missing, set them to full screen width | |
201 | ||
202 | newRect.x1 = simpleRect.x and (simpleRect.x > -1 and simpleRect.x or (df.global.gps.dimx+simpleRect.x)) or 0 | |
203 | newRect.y1 = simpleRect.y and (simpleRect.y > -1 and simpleRect.y or (df.global.gps.dimy+simpleRect.y)) or 0 | |
204 | --wrapping around the screen, and zeroes if not initialized | |
205 | newRect.x2=newRect.x1+newRect.width - 1 | |
206 | newRect.y2=newRect.y1+newRect.height - 1 | |
207 | newRect.clip_x1=newRect.x1 | |
208 | newRect.clip_x2=newRect.x2 | |
209 | newRect.clip_y1=newRect.y1 | |
210 | newRect.clip_y2=newRect.y2 | |
211 | newRect.x=newRect.x1 | |
212 | newRect.y=newRect.y1 | |
213 | newRect.hgap=newRect.height -- if both hgap and wgap are 0, it'll set the screen to black. | |
214 | newRect.wgap=newRect.width -- that's a little undesirable loop if I'm creating, not dismissing | |
215 | if (newRect.hgap == 0) and (newRect.hgap == newRect.wgap) then newRect.hgap = 1 end | |
216 | -- preventing it allows zero-size frames without blanking the screen. | |
217 | ||
218 | return newRect | |
219 | end | |
220 | ||
221 | ||
222 | function set_rect(target, corrector) | |
223 | -- sets all corrector key values found in targetrect to corrector ones | |
224 | -- This softness is useful for avoiding table bloat or modifying native userdata | |
225 | for key, value in pairs(corrector) do | |
226 | if target[key] then target[key] = value end | |
227 | end | |
228 | end | |
229 | ||
230 | textrect = createRect(simpleTextRect) | |
231 | framerect = simpleFrameRect and createRect(simpleFrameRect) or false | |
232 | ||
233 | local zeroRect = {width = 0, height = 0, x = 0, y = 0} --Declared for setRects to prevent table proliferation | |
234 | function screen:setRects(textrect, framerect) | |
235 | --sets screen's dimension values to given input rects | |
236 | set_rect(self.frame_body, textrect) | |
237 | if framerect then | |
238 | set_rect(self.frame_rect, framerect) | |
239 | else | |
240 | set_rect(self.frame_rect, createRect(zeroRect)) | |
241 | --Even without instructions for frame body, it is still set. | |
242 | --Also dfhack indicator depends on it existing somewhere, so can't just skip drawing frame. | |
243 | end | |
244 | end | |
245 | ||
246 | ||
247 | local inheritedbindings = {} | |
248 | local modifiertable = {} | |
249 | --(Static) table for all possible keybinding modifiers | |
250 | table.insert(modifiertable, "") --nothing too | |
251 | table.insert(modifiertable, "Ctrl-") | |
252 | table.insert(modifiertable, "Alt-") | |
253 | table.insert(modifiertable, "Shift-") | |
254 | table.insert(modifiertable, "Ctrl-Alt-") | |
255 | table.insert(modifiertable, "Ctrl-Shift-") | |
256 | table.insert(modifiertable, "Shift-Alt-") | |
257 | table.insert(modifiertable, "Ctrl-Alt-Shift") | |
258 | function screen:inheritBindings() | |
259 | --Asks keybinding for all keybindings in parent context, and binds them to itself. | |
260 | if self.binding_inherit and not (self.binding_inherit:find("none")) then | |
261 | local bindinglist = {} | |
262 | local context = dfhack.gui.getFocusString(self._native.parent) | |
263 | ||
264 | function insertAllKeymods(key, searched_context) | |
265 | --Takes a single key (e.g. K) and checks if it with any and all modifiers applies to searched context | |
266 | --If pplies, adds that to binding list | |
267 | for i = 1, #modifiertable do | |
268 | local keybinding_text = dfhack.run_command_silent("keybinding list " .. modifiertable[i] .. key) | |
269 | if not keybinding_text:find("No bindings") then --No bindings, ergo nothing to do | |
270 | for single_bind in keybinding_text:gmatch("%s%s[^%s].-\n") do --Split up the list and iterate it. | |
271 | local received_context = single_bind:match("@.-:") --Ask what context given command uses | |
272 | if not received_context or --general, DF-wide context | |
273 | searched_context:find(received_context:sub(2,-2)) then --context matches same or lower screen | |
274 | ||
275 | table.insert(bindinglist, modifiertable[i] .. key --modifier-key combination | |
276 | .. (received_context and single_bind:sub(3,-2)--binding has context (matters for @_:) | |
277 | or ("@_: " .. single_bind:sub(3,-2) )) .. '"')--binding doesn't have context+" at end. | |
278 | end | |
279 | end | |
280 | end | |
281 | end | |
282 | end | |
283 | ||
284 | for charCode = 65, 90 do --A to Z | |
285 | insertAllKeymods(string.char(charCode), context) | |
286 | end | |
287 | for i=0, 9 do --0 to 9 | |
288 | insertAllKeymods(tostring(i), context) | |
289 | end | |
290 | for i=0, 12 do --F1 to F12 | |
291 | insertAllKeymods("F" .. tostring(i), context) | |
292 | end | |
293 | insertAllKeymods("Enter", context) --Enter is only non-alpa(F)numeric key. | |
294 | ||
295 | for index = 1, #bindinglist do | |
296 | local inheritedbinding = bindinglist[index] | |
297 | :gsub("\n",'"') | |
298 | :gsub("@.-: ", | |
299 | ("@dfhack/lua/" .. self.focus_path .. (self.binding_inherit == "hook" and ' "gui/indicator_screen execute_hook ' or ' "'))) | |
300 | table.insert(inheritedbindings, inheritedbinding) | |
301 | dfhack.run_command("keybinding add " .. inheritedbinding) | |
302 | end | |
303 | bindinglist = nil --cleanup | |
304 | ||
305 | end | |
306 | end | |
307 | ||
308 | function screen:clearBindings() | |
309 | --function to clear up previously added keybindings | |
310 | if self.binding_inherit and | |
311 | (self.binding_inherit == "keybinding" or self.binding_inherit == "hook") then | |
312 | for index = 1, #inheritedbindings do | |
313 | dfhack.run_command_silent("keybinding clear " .. inheritedbindings[index]) | |
314 | end | |
315 | end | |
316 | inheritedbindings = {} --Bye old table, hello new table | |
317 | end | |
318 | ||
319 | function screen:onDestroy() | |
320 | --Screen is removed, so better clean up keybinding list. | |
321 | self:clearBindings() | |
322 | self._native = nil | |
323 | --Potentially dangerous technique that should reduce RAM usage due C not having any garbage collection. | |
324 | inheritedbindings = nil --no longer needed | |
325 | end | |
326 | ||
327 | -- Utility function | |
328 | -- Due dynamic tree structure, don't know where desired viewscreen is? Retrieve it | |
329 | function getBottomMostViewscreenWithFocus(text, targetscreen) | |
330 | --Finds and returns the screen closest to root screen whose path includes text | |
331 | if targetscreen and | |
332 | dfhack.gui.getFocusString(targetscreen):find(text) then | |
333 | return targetscreen | |
334 | elseif targetscreen and targetscreen.child then --Attempting to call nil.child will crash this | |
335 | return getBottomMostViewscreenWithFocus(text, targetscreen.child) | |
336 | end | |
337 | -- if there's no child, it didn't find a screen with text in focus and returns nil | |
338 | end | |
339 | ||
340 | ||
341 | -- Replacement for onGetSelectedUnit and onGetSelectedJob | |
342 | -- Necessary for View unit, unitlist, joblist | |
343 | local function onGetSelectedX(X) | |
344 | ||
345 | --View mode | |
346 | if getBottomMostViewscreenWithFocus("dwarfmode/ViewUnits/Some", df.global.gview.view.child) then | |
347 | if X == "units" then | |
348 | return df.global.world.units.active[df.global.ui_selected_unit] | |
349 | elseif X == "jobs" then | |
350 | return df.global.world.units.active[df.global.ui_selected_unit].job.current_job | |
351 | end | |
352 | end | |
353 | ||
354 | --Workshop mode | |
355 | local workshopscr = getBottomMostViewscreenWithFocus("QueryBuilding/Some/Workshop/Job", df.global.gview.view.child) | |
356 | if workshopscr then | |
357 | if X == "jobs" then | |
358 | return df.global.ui_sidebar_menus.workshop_job.choices_all[0].building --workshop | |
359 | .jobs[df.global.ui_workshop_job_cursor] --currently selected job in workshop | |
360 | elseif X == "units" then | |
361 | local jobrefs= | |
362 | df.global.ui_sidebar_menus.workshop_job.choices_all[0].building --workshop | |
363 | .jobs[df.global.ui_workshop_job_cursor] --selected job | |
364 | .general_refs | |
365 | for index, ref in pairs(jobrefs) do | |
366 | if ref._type == df.general_ref_unit_workerst then | |
367 | return df.unit.find(ref.unit_id) | |
368 | end | |
369 | end | |
370 | end | |
371 | end | |
372 | ||
373 | --joblist, unitlistmode | |
374 | local joblist = getBottomMostViewscreenWithFocus("joblist",df.global.gview.view.child) | |
375 | local unitlist = getBottomMostViewscreenWithFocus("unitlist",df.global.gview.view.child) | |
376 | if joblist then | |
377 | if X == "units" then | |
378 | if joblist.jobs | |
379 | [joblist.cursor_pos] then | |
380 | local jobrefs = | |
381 | joblist.jobs | |
382 | [joblist.cursor_pos] | |
383 | .general_refs | |
384 | for index, ref in pairs(jobrefs) do | |
385 | if ref._type == df.general_ref_unit_workerst then | |
386 | return df.unit.find(ref.unit_id) | |
387 | end | |
388 | end | |
389 | else | |
390 | return joblist[X] | |
391 | [joblist.cursor_pos] | |
392 | end | |
393 | end | |
394 | if X == "jobs" then | |
395 | return joblist[X] | |
396 | [joblist.cursor_pos] | |
397 | end | |
398 | elseif unitlist then | |
399 | local whichlist | |
400 | if unitlist.page == 0 then | |
401 | whichlist = "Citizens" | |
402 | elseif unitlist.page == 1 then | |
403 | whichlist = "Livestock" | |
404 | elseif unitlist.page == 2 then | |
405 | whichlist = "Others" | |
406 | else | |
407 | whichlist = "Dead" --dead taking our jobs. Say it ain't so! | |
408 | end | |
409 | ||
410 | return unitlist[X] | |
411 | [whichlist] | |
412 | [unitlist.cursor_pos[whichlist]] | |
413 | end | |
414 | ||
415 | end | |
416 | ||
417 | local function setOnSelectedUnitJob(self) | |
418 | if getBottomMostViewscreenWithFocus("joblist",df.global.gview.view.child) or | |
419 | getBottomMostViewscreenWithFocus("unitlist",df.global.gview.view.child) or | |
420 | getBottomMostViewscreenWithFocus("QueryBuilding/Some/Workshop/Job", df.global.gview.view.child) or | |
421 | getBottomMostViewscreenWithFocus("dwarfmode/ViewUnits/Some", df.global.gview.view.child) then | |
422 | self.onGetSelectedUnit = function() return onGetSelectedX("units") end | |
423 | self.onGetSelectedJob = function() return onGetSelectedX("jobs") end | |
424 | end | |
425 | end | |
426 | ||
427 | function screen:onShow() | |
428 | self.focus_path = "indicator_screen/" .. dfhack.gui.getFocusString(self._native.parent) | |
429 | --Doesn't have a parent before showing | |
430 | self:setRects(textrect, framerect) | |
431 | self.dismiss_ordered = false | |
432 | --Lets not dismiss the screen the first time we look away, mkay. | |
433 | if self.binding_inherit and | |
434 | (self.binding_inherit == "keybinding" or self.binding_inherit == "hook") then | |
435 | --If screen is set to inherit bindings, set the appropriate keybindings. | |
436 | self:inheritBindings() | |
437 | end | |
438 | -- Additional gui functions, but only in contexts where they matter. | |
439 | -- Also checked/added in onInput | |
440 | setOnSelectedUnitJob(self) | |
441 | ||
442 | -- low-FPS/GFPS ratio concerns. | |
443 | -- As it is, does ultimately nothing if screen.dismiss_ordered isn't modified later | |
444 | -- because all the pre-set dissmisses are already on viewscreen changes. | |
445 | if self.nonblinky_dismiss then | |
446 | self.onStateChangeKey = "indicator_screen_" .. tostring(self) .. "_nonblinky" | |
447 | --Unique, reproducible, maybe-useful key for onstateChange | |
448 | --Using self itself as key will prevent garbage collection and be not as understandable as "what's this" | |
449 | function orderDismissOnScreenChange(code) | |
450 | if not self or not self:isActive() then | |
451 | --shenagians like v-z-r-z can poof the screens | |
452 | dfhack.onStateChange[self.onStateChangeKey] = nil | |
453 | if self and not self:isDismissed() then self:dismiss() end | |
454 | -- does away with non-shown screen, if I have one. | |
455 | elseif code == SC_VIEWSCREEN_CHANGED --changed viewscreen | |
456 | and self.dismiss_ordered then --this generated screen has been ordered to disappear | |
457 | self:dismiss() | |
458 | dfhack.onStateChange[self.onStateChangeKey] = nil --clean up global namespace after use | |
459 | end | |
460 | end | |
461 | dfhack.onStateChange[self.onStateChangeKey] = orderDismissOnScreenChange | |
462 | end | |
463 | ||
464 | end | |
465 | ||
466 | function screen:onResize() self:setRects(textrect, framerect) end | |
467 | ||
468 | ||
469 | -- initialize display area | |
470 | ||
471 | ||
472 | -- onRenderBody: has support for either plaintext/number values or function values | |
473 | -- Note that due getLongestLength each text function will be called once even without showing the screen. | |
474 | -- since dynamic_text_table is a table, the values in it will change when something else modifies them. | |
475 | -- could reduce overhead by only checking which ones are functions at start- but then it is not as dynamic. | |
476 | local emptyKeyTable, onRenderi, cury,curx1,curx2,mousey,mousex,useGeneral = {} | |
477 | --Some variables used in onRenderBody left outside to reduce footprint. | |
478 | local function renderText(dc, dynamic_text, notnewline) | |
479 | dc:string( | |
480 | (type(dynamic_text.text) == "function") and dynamic_text.text() or dynamic_text.text, | |
481 | (type(dynamic_text.color) == "function") and dynamic_text.color() or dynamic_text.color | |
482 | ) | |
483 | if not notnewline then dc:newline() end | |
484 | end | |
485 | ||
486 | function screen:onRenderBody(dc) | |
487 | --DF sometimes needs a poke to catch up to changes in viewscreen ordering | |
488 | if self.focus_path ~= ("indicator_screen/" .. dfhack.gui.getFocusString(self._native.parent)) or | |
489 | ("dfhack/lua/" .. self.focus_path ) ~= dfhack.gui.getFocusString(self._native) or --probaby unnecessary | |
490 | self.binding_inherit == "hook" and self._native.parent.breakdown_level == 2 then | |
491 | --While the logic is generally done in onInput for this infrequent problem, before rendering next frame | |
492 | gui.simulateInput(self._native, emptyKeyTable) | |
493 | -- the viewscreen doesn't update it's focus path properly without poking it. | |
494 | end | |
495 | --Rendering section | |
496 | if dynamic_text_table.color then dc:pen(dynamic_text_table.color) end | |
497 | mousey = df.global.gps.mouse_y | |
498 | mousex = df.global.gps.mouse_x | |
499 | cury,curx1,curx2, useGeneral = self.frame_body.y1, self.frame_body.x1, self.frame_body.x1, | |
500 | (mousey >= self.frame_body.y1 and mousey <= self.frame_body.y2) and | |
501 | (mousex >= self.frame_body.x1 and mousex <= self.frame_body.x2) and true or false | |
502 | for onRenderi=1, #dynamic_text_table do | |
503 | if (mousey >= self.frame_body.y1 and | |
504 | mousey <= self.frame_body.y2) and | |
505 | (mousex >= self.frame_body.x1 and | |
506 | mousex <= self.frame_body.x2) then | |
507 | --A mouse over the frame. Does it do something? | |
508 | --First, lets give priority to specific parts of frame. | |
509 | curx2 = curx1 -1 + #((type(dynamic_text_table[onRenderi].text) == "function") and | |
510 | dynamic_text_table[onRenderi].text() | |
511 | or dynamic_text_table[onRenderi].text) | |
512 | --Determining the dimensions of given dynamic text | |
513 | if mousey == cury and | |
514 | (mousex >= curx1 and | |
515 | mousex <= curx2) then | |
516 | --There's an overlap with text. Commencing hover function and text adding | |
517 | --Something of an issue is how it still uses non-rendered text dimensions | |
518 | --That can even result in multiple onhovers being called simultaneously. | |
519 | --Blinking and such alternatives aren't too good either, so it stays. | |
520 | if dynamic_text_table[onRenderi].onhoverfunction and | |
521 | type(dynamic_text_table[onRenderi].onhoverfunction) == "function" then | |
522 | useGeneral = false | |
523 | dynamic_text_table[onRenderi].onhoverfunction() | |
524 | end | |
525 | if dynamic_text_table[onRenderi].onhovertext and | |
526 | type(dynamic_text_table[onRenderi].onhovertext) == "table" then | |
527 | curx2 = curx2 +1 - #((type(dynamic_text_table[onRenderi].text) == "function") and | |
528 | dynamic_text_table[onRenderi].text() | |
529 | or dynamic_text_table[onRenderi].text) | |
530 | + #((type(dynamic_text_table[onRenderi].onhovertext.text) == "function") and | |
531 | dynamic_text_table[onRenderi].onhovertext.text() | |
532 | or dynamic_text_table[onRenderi].onhovertext.text) | |
533 | --Got to readjust x if I rendered hovertext | |
534 | renderText(dc, dynamic_text_table[onRenderi].onhovertext, dynamic_text_table[onRenderi].notEndOfLine) | |
535 | --Using old notEndOfLine for arguably greater convenience. | |
536 | --If onhovertext was fully independent text element, it should be different, but it isn't. | |
537 | else | |
538 | renderText(dc, dynamic_text_table[onRenderi], dynamic_text_table[onRenderi].notEndOfLine) | |
539 | end | |
540 | else | |
541 | renderText(dc, dynamic_text_table[onRenderi], dynamic_text_table[onRenderi].notEndOfLine) | |
542 | end | |
543 | if not dynamic_text_table[onRenderi].notEndOfLine then cury = cury + 1 end | |
544 | if dynamic_text_table[onRenderi].notEndOfLine then curx1 = curx2+1 else curx1 = self.frame_body.x1 end | |
545 | else --Mouse isn't over frame at all, proceed as normal | |
546 | renderText(dc, dynamic_text_table[onRenderi], dynamic_text_table[onRenderi].notEndOfLine) | |
547 | end | |
548 | end | |
549 | --General hover comes in in case specific ones didn't apply. | |
550 | --Because it is far easier to apply general anyway after - or before - specific in relevant function, | |
551 | --than it is to block general from applying. | |
552 | if useGeneral and dynamic_text_table.onhoverfunction and | |
553 | type(dynamic_text_table.onhoverfunction) == "function" then | |
554 | dynamic_text_table.onhoverfunction() | |
555 | end | |
556 | ||
557 | end | |
558 | ||
559 | local initial_view_child = df.global.gview.view.child.child | |
560 | --Must check current subview before I plant a new subview to see whether it is nil | |
561 | local localpen = {ch, fg, bg, bold, tile, tile_color, tile_fg, tile_bg} | |
562 | for i, value in pairs(screen.frame_style) do | |
563 | localpen[i] = value | |
564 | end | |
565 | --Must not edit a global pen, and doing this right below would do new {} every frame. | |
566 | --disadvantage is that must later edit localpen, not self.frame_style | |
567 | screen.signature_text = "DFHack" | |
568 | function paint_signature(castx, casty, style, self) | |
569 | -- Places dfhack indicator on closest border, either verticall or horizontally | |
570 | -- Used when not rendering the rest of frame | |
571 | -- signature defines if it is called normally, called with gray on gray or not called for true, nil, false | |
572 | if(self.signature == true or self.signature == nil) then | |
573 | local MiddleFrame = (not initial_view_child) -- not nil (== true) if this is first screen. | |
574 | and (isWideView() -- In that case, lets check if view is wide, | |
575 | and (df.global.gps.dimx-56) -- and if it is, return it's bar's location, | |
576 | or (isWideView()==false -- and it isn't, if it is thin | |
577 | and (df.global.gps.dimx-32) -- and therefore thin bar's location | |
578 | or 0)) -- or perhaps left edge if it isn't thin too. | |
579 | or 0 -- Tho subordinate screens typically don't have divider. | |
580 | --if there's a child frame, there isn't a viewscreen present. Unless it's dfhack viewscreen | |
581 | -- Potential todo: Elegant handling several indicator screens in same location. | |
582 | -- Currently, it tends to stack the dfhacks on top of each other for multiple indicator screens. | |
583 | -- Might be preferable to having several of them, though, so keeping current behaviour | |
584 | localpen.fg = self.signature and 16 or 8 | |
585 | localpen.bg = 8 | |
586 | local Hcloseness = casty > --Is the drawn screen N side | |
587 | (df.global.gps.dimy - casty)-- closer to | |
588 | and df.global.gps.dimy -- Bottom window border | |
589 | or 0 -- or Top window border? | |
590 | ||
591 | local Vcloseness = (df.global.gps.dimx-castx) < -- Is the drawn screen W side | |
592 | (castx-MiddleFrame ) -- closest to | |
593 | and df.global.gps.dimx -- Right window border? | |
594 | or ((MiddleFrame - castx) < castx -- Else is it closest to | |
595 | and MiddleFrame -- Middle divider | |
596 | or 0 ) -- or Left window border? | |
597 | if math.abs(Vcloseness - castx) < math.abs(Hcloseness-casty) then | |
598 | x = Vcloseness | |
599 | y = casty | |
600 | for index = 1, #screen.signature_text do | |
601 | dfhack.screen.paintString(localpen,x,y+index-1,string.sub(screen.signature_text,index,index)) | |
602 | end | |
603 | else | |
604 | x = castx | |
605 | y = Hcloseness | |
606 | dfhack.screen.paintString(localpen,x,y, screen.signature_text) | |
607 | end | |
608 | end | |
609 | end | |
610 | ||
611 | -- Overshadowing gui function, to paint signature for cases of no inset. | |
612 | -- Though worth noting that that function paints the frame even for inset -1, just behind the text screen usually. | |
613 | ||
614 | function screen:onRenderFrame(dc, rect) | |
615 | local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2 | |
616 | ||
617 | if rect.wgap <= 0 and rect.hgap <= 0 then | |
618 | dc:clear() | |
619 | else | |
620 | self:renderParent() | |
621 | dc:fill(rect, self.frame_background) | |
622 | end | |
623 | if self.frame_inset > -1 then gui.paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title) | |
624 | else | |
625 | paint_signature(self.frame_body.x1, self.frame_body.y1,self.frame_style, self) | |
626 | end | |
627 | end | |
628 | ||
629 | -- A dynamic input rectangle could permit adjusting dims as well, but would need constantly rechecking values | |
630 | -- Alternatively, to change things only as necessay, a function to set them. | |
631 | -- Asks for if you alter body or frame, then takes upper left corner position and width | |
632 | -- values not set will be default | |
633 | -- to adjust only, say, width, you'd use yourscreen:adjustDims(true, nil, nil, newwidth) | |
634 | -- could create new indicatoror on adjusting those values. | |
635 | function screen:adjustDims(affectsText,x,y,w,h) | |
636 | local targetscreen = affectsText and "frame_body" or "frame_rect" | |
637 | local simpleRect = { | |
638 | x = x or self[targetscreen].x1, | |
639 | y = y or self[targetscreen].y1, | |
640 | width = w or self[targetscreen].width, | |
641 | height = h or self[targetscreen].height | |
642 | } | |
643 | set_rect(self[targetscreen],createRect(simpleRect)) | |
644 | end | |
645 | ||
646 | --If I ever want to remove it from view without, yknow, flickering on dismiss. | |
647 | --Limited use, as what are you going to do, just leave them all there? Going to create feature-creep eventually | |
648 | --Maybe use with init setting less_flickering to, say, set timeout to dismiss on every thousandth frame/second | |
649 | --Alternative solution to flickering: whenever exiting a (vanilla) viewscreen, mask behind its flicker. | |
650 | --Possibly setting an auto-clean if there's, say, 100 indicator screens already displayed | |
651 | function screen:removeFromView(dissmiss_when) | |
652 | for i,k in pairs(self.frame_rect) do | |
653 | self.frame_rect[i] = -10 | |
654 | end | |
655 | for i,k in pairs(self.frame_body) do | |
656 | self.frame_body[i] = -10 | |
657 | end | |
658 | self.frame_rect.wgap = 1 | |
659 | self.dismiss_ordered=true | |
660 | --Works with enabled nonblinky dismiss | |
661 | self.signature = nil | |
662 | --Still subtly visible, but the screen is hidden, so it shouldn't be obvious | |
663 | if dismiss_when then | |
664 | dfhack.timeout(dismiss_when,"ticks", function () self:dismiss() end) | |
665 | end | |
666 | end | |
667 | ||
668 | function screen:onHelp() | |
669 | --Placeholder function | |
670 | --Dismisses self as to not make it impossible to ask for native help while screen is up | |
671 | self:dismiss() | |
672 | end | |
673 | ||
674 | ||
675 | function screen:onInput(keys) | |
676 | -- Basic input reactor | |
677 | -- One may subsume this function on customizing, but it already does quite a lot. | |
678 | ||
679 | ||
680 | --More of a syntax simplifier function | |
681 | local function parentHas(text) | |
682 | return(dfhack.gui.getFocusString(self._native.parent):find(text)) | |
683 | end | |
684 | ||
685 | --onclick & onrclick implementation | |
686 | if ((keys._MOUSE_L or keys._MOUSE_L_DOWN) or | |
687 | (keys._MOUSE_R or keys._MOUSE_R_DOWN)) and | |
688 | (df.global.gps.mouse_y >= self.frame_body.y1 and | |
689 | df.global.gps.mouse_y <= self.frame_body.y2) and | |
690 | (df.global.gps.mouse_x >= self.frame_body.x1 and | |
691 | df.global.gps.mouse_x <= self.frame_body.x2) then | |
692 | --A click over the frame. Does it do something? | |
693 | --First, lets give priority to specific parts of frame. | |
694 | cury,curx1,curx2, useGeneral = self.frame_body.y1, self.frame_body.x1, self.frame_body.x1, true | |
695 | for i=1, #dynamic_text_table do | |
696 | curx2 = curx1 -1 + #((type(dynamic_text_table[i].text) == "function") and | |
697 | dynamic_text_table[i].text() | |
698 | or dynamic_text_table[i].text) | |
699 | if df.global.gps.mouse_y == cury and | |
700 | (df.global.gps.mouse_x >= curx1 and | |
701 | df.global.gps.mouse_x <= curx2) then | |
702 | --Single character has y1 == y2, empty string y2<y1 | |
703 | if (keys._MOUSE_L or keys._MOUSE_L_DOWN) and | |
704 | dynamic_text_table[i].onclick and | |
705 | type(dynamic_text_table[i].onclick) == "function" then | |
706 | --left click | |
707 | useGeneral = false | |
708 | dynamic_text_table[i].onclick() | |
709 | elseif (keys._MOUSE_R or keys._MOUSE_R_DOWN) and | |
710 | dynamic_text_table[i].onrclick and | |
711 | type(dynamic_text_table[i].onrclick) == "function" then | |
712 | --right click | |
713 | useGeneral = false | |
714 | dynamic_text_table[i].onrclick() | |
715 | end | |
716 | break; --onclick or no, no point in looking further when found the overlap. | |
717 | end | |
718 | if not dynamic_text_table[i].notEndOfLine then cury = cury + 1 end | |
719 | if dynamic_text_table[i].notEndOfLine then curx1 = curx2+1 else curx1 = self.frame_body.x1 end | |
720 | end | |
721 | --General clicks come in in case specific ones didn't apply. | |
722 | --Because it is far easier to apply general anyway after - or before - specific in relevant function, | |
723 | --than it is to block general from applying. | |
724 | if useGeneral and | |
725 | (keys._MOUSE_L or keys._MOUSE_L_DOWN) and | |
726 | self.onclick and | |
727 | type(self.onclick) == "function" then | |
728 | self.onclick() | |
729 | elseif useGeneral and | |
730 | (keys._MOUSE_R or keys._MOUSE_R_DOWN) and | |
731 | self.onrclick and | |
732 | type(self.onrclick) == "function" then | |
733 | self.onrclick() | |
734 | end | |
735 | end | |
736 | -- Menu scrolling fix | |
737 | --higher numbers mean multi-stacked indicator screens. I only need to fix cursor once. | |
738 | if (parentHas("dwarfmode") == 1 and | |
739 | not parentHas("dwarfmode/Build/Material/Groups")) | |
740 | and ( | |
741 | keys.SECONDSCROLL_DOWN or --actually pressed a key I didn't want to change navigation | |
742 | keys.SECONDSCROLL_UP or | |
743 | keys.SECONDSCROLL_PAGEDOWN or | |
744 | keys.SECONDSCROLL_PAGEUP ) then | |
745 | if(keys.SECONDSCROLL_DOWN) then | |
746 | df.global.cursor.x = df.global.cursor.x+1 | |
747 | df.global.cursor.y = df.global.cursor.y-1 | |
748 | end | |
749 | if(keys.SECONDSCROLL_UP) then | |
750 | df.global.cursor.y = df.global.cursor.y+10 | |
751 | ||
752 | end | |
753 | if(keys.SECONDSCROLL_PAGEDOWN) then | |
754 | df.global.cursor.x = df.global.cursor.x -1 | |
755 | df.global.cursor.y = df.global.cursor.y +1 | |
756 | ||
757 | end | |
758 | if(keys.SECONDSCROLL_PAGEUP) then | |
759 | df.global.cursor.x = df.global.cursor.x+10 | |
760 | end | |
761 | --originally had a section here that reset cursor position to within map area... | |
762 | --But game does that by itself. | |
763 | --TODO: Iirc there were some screens without cursor where the above adjustment resulted | |
764 | -- in taking the hidden x -30000 and making it jump there after exiting and using v. | |
765 | ||
766 | ||
767 | --Some sections don't move cursor, but affect df.global.window_x,y instead | |
768 | local windowx = df.global.window_x | |
769 | local windowy = df.global.window_y | |
770 | self:sendInputToParent(keys) | |
771 | df.global.window_x = windowx | |
772 | df.global.window_y = windowy | |
773 | else | |
774 | self:sendInputToParent(keys) | |
775 | end | |
776 | ||
777 | --In some cases, screen must be removed for DF hotkey to function | |
778 | if parentHas("dwarfmode/Default") and (keys.LEAVESCREEN or --Can't Escape into savegame menu with screen | |
779 | keys.D_ONESTEP or D_PAUSE or --DF doesn't run unpaused when screen is up. | |
780 | keys.UNITVIEW_FOLLOW) then --Can't follow unit around either | |
781 | self:dismiss() | |
782 | --Any replacing of screens onto applicable areas has to be handled by planting a new screen probs. | |
783 | end | |
784 | ||
785 | --Navigation: Leaving entered menus: | |
786 | if keys.LEAVESCREEN and (not parentHas("dwarfmode")) or | |
787 | -- Navigation woes: You can't bind Escape nor navigate "down" while screen is up | |
788 | -- That mostly means zooming, I think | |
789 | ||
790 | -- With z: | |
791 | ( (not parentHas("dwarfmode")) and (--Don't want to cancel screen because z-ing an unit or kitchen | |
792 | --Since this is "not in dwarfmode", z will cancel the screen even where it'd normally do nothing. | |
793 | ||
794 | (--keys.D_HOTKEY_ZOOM or --Hotkeys to enable zooming with F1-8 to place | |
795 | keys.UNITVIEW_RELATIONSHIPS_ZOOM or--relationships zoom | |
796 | keys.UNITJOB_ZOOM_CRE or --unitlist, joblist | |
797 | keys.ANNOUNCE_ZOOM or --(a)nnouncement list, reports | |
798 | keys.STORES_ZOOM or --effect unknown | |
799 | keys.A_LOG_ZOOM_SELECTED ) --effect unknown | |
800 | or --With b, in unitlist and joblist only: | |
801 | (keys.UNITJOB_ZOOM_BUILD and | |
802 | (parentHas("unitlist") or | |
803 | parentHas("joblist") | |
804 | )) | |
805 | or --with q and t, in buildinglist only: | |
806 | (parentHas("buildinglist") and | |
807 | ((keys.BUILDINGLIST_ZOOM_Q) | |
808 | or | |
809 | (keys.BUILDINGLIST_ZOOM_T))) | |
810 | --or OPTION2-20+ --What are these for? | |
811 | ) ) then | |
812 | if self.dismiss_on_zoom then | |
813 | self:dismiss() | |
814 | elseif not dfhack.gui.getFocusString(self._native.parent):find("indicator_screen") then | |
815 | --only have to re-place the lowest indicator screen; otherwise others will become lost. | |
816 | self._native.parent.breakdown_level = 2 | |
817 | end | |
818 | end | |
819 | -- With navigation or hooking, one will carry over bindings intended for grandparent indicator screen. | |
820 | -- That can be useful - i.e. going gui/dfstatus then stocks show - but is not intended and can be detrimental | |
821 | -- with starting from default and then using v-navigation for instance. | |
822 | if self.focus_path ~= ("indicator_screen/" .. dfhack.gui.getFocusString(self._native.parent)) then | |
823 | self.focus_path = "indicator_screen/" .. dfhack.gui.getFocusString(self._native.parent) | |
824 | self:clearBindings() | |
825 | self:inheritBindings() | |
826 | end | |
827 | if self._native.parent.breakdown_level == 2 and not self:isDismissed() then | |
828 | --In case of hooks and zooming, the dismissed screen can be left hanging and screw things up. | |
829 | self._native.parent.parent.child = self._native.parent.child | |
830 | self._native.parent = self._native.parent.parent | |
831 | --Gotta move the screen one upwards. | |
832 | end | |
833 | -- Script support checks that context of the functions is still apppropriate. | |
834 | -- If one dismisses on zoom, would matter only if there's a preexisting screen present | |
835 | -- as one enters relevant contexts. | |
836 | -- Does rely on the assumption that one uses keyboard or mouse (instead of console) to navigate. | |
837 | setOnSelectedUnitJob(self) | |
838 | ||
839 | end | |
840 | ||
841 | return screen | |
842 | end | |
843 | ||
844 | function placeOnTop(domScreen) | |
845 | --Takes a domScreen, and places it on top of currently topmost screen. | |
846 | local subScreen = dfhack.gui.getCurViewscreen() | |
847 | --Maybe this local variable name might be a little mean... | |
848 | domScreen.parent=subScreen | |
849 | --It's not the worst possible name, considering. | |
850 | subScreen.child=domScreen | |
851 | end | |
852 | ||
853 | local args = {...} | |
854 | ||
855 | if args and args[1] == "help" then print(helptext) | |
856 | elseif args and args[1] == "execute_hook" then | |
857 | local execute_helptext = [[ | |
858 | indicator_screen execute_hook | |
859 | ============================= | |
860 | ||
861 | Utility command for indicator-screen module. | |
862 | Temporarily removes bottommost scren with indicator_screen focus path, | |
863 | launchs a command given, then adds the screen back on top after that | |
864 | ||
865 | Usage format: | |
866 | gui/indicator_screen execute_hook <valid command to keybind> | |
867 | ||
868 | Intended to be used with keybinding so that the command is executed | |
869 | in context below bottommost indicator screen. | |
870 | ]] | |
871 | if #args == 1 or --No command given, | |
872 | (tostring(args[2]):find("help") and (#(tostring(args[2]))<6)) or --asking for help, | |
873 | (tostring(args[2]):find("?") and (#(tostring(args[2]))<3)) then -- or being confused? | |
874 | dfhack.print(execute_helptext) -- Explain! | |
875 | else | |
876 | local gui = require 'gui' | |
877 | ||
878 | function getIndicatorScreenChainBase(targetscreen) | |
879 | --Finds and returns the screen farthest from parent screen whose focus path includes indicator_screen | |
880 | --Not closest to root screen, as that leads to weirdness with screen>unitlist>screen>manipulator | |
881 | if targetscreen and --Recursive function may run into nil | |
882 | dfhack.gui.getFocusString(targetscreen):find("indicator_screen") and --the topmost is what we want | |
883 | not dfhack.gui.getFocusString(targetscreen.parent):find("indicator_screen") --and it's parent is not | |
884 | then | |
885 | return targetscreen | |
886 | elseif targetscreen and targetscreen.parent then --Attempting to call nil.parent will crash this | |
887 | return getIndicatorScreenChainBase(targetscreen.parent) | |
888 | end | |
889 | -- if there's no parent, it didn't find an indicator screen and returns nil | |
890 | end | |
891 | ||
892 | local lowestIndicatorScreen = getIndicatorScreenChainBase(dfhack.gui.getCurViewscreen()) | |
893 | --only if we find any indicator screens do we execute the command | |
894 | if lowestIndicatorScreen then | |
895 | lowestIndicatorScreen.parent.child = nil | |
896 | -- Return execution environment to pre-screen context before execution | |
897 | dfhack.timeout(1, "frames", function() -- Wait a bit for dfhack to catch up | |
898 | dfhack.run_command(table.concat(args, " ",2)) --Execute command in base environment | |
899 | --Needs spaces in-between arguments | |
900 | placeOnTop(lowestIndicatorScreen) --Place the indicator screen back on top. | |
901 | end) | |
902 | end | |
903 | end | |
904 | ||
905 | end |