Advertisement
Guest User

gui/indicator-screen 1.2 [44.09+ support]

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