Advertisement
Guest User

[Dwarf Fortress] Fleeting Frames' gui/indicator_screen

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