Advertisement
Guest User

[Dwarf Fortress] Fleeting Frames' gui/indicator_screen 1.1

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