View difference between Paste ID: AyRYKc3L and s6H51mP7
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