theTANCO

ContextMenu.lua

Jun 27th, 2022 (edited)
1,106
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.69 KB | None | 0 0
  1. -- Custom context menu creator for ComputerCraft.
  2. -- Run the following commands before using this file:
  3. --   pastebin get Rac6Jxjg "/API/LibAppend.lua"
  4. --   pastebin get KA2dK07y "/API/Events.lua"
  5. --   pastebin get t2TvSiSU "/API/Class.lua"
  6. --   pastebin get 4hpwn3mn "/API/ContextMenu.lua"
  7. -- Documentation and the changelog for this class can be found below.
  8. require("/API/Events")
  9.  
  10. ContextMenu = Class(function()
  11. local events = Events() -- Primarily for processing user input.
  12. local pressed = "" -- The name of the menu button currently being pressed.
  13. local buttons = {} -- Positions of buttons.
  14. local disabled = {} -- Functions that check if buttons are disabled.
  15. local dir = {left = {}, right = {}, up = {}, down = {}} -- Names of adjacent buttons.
  16. local pos = vector.new() -- Position of the menu.
  17. local size = vector.new() -- Size of the menu.
  18. local color = {} -- All color values.
  19. local alt = "" -- The button that is selected when 'alt' is pressed.
  20. local win = {} -- The window for the menu.
  21. local ro = {} -- All of the readonly methods/properties for the menu object.
  22.  
  23. -- Processes the graphics for a button being unpressed.
  24. local unpress = function()
  25.     local button = pressed
  26.     if button ~= "" then
  27.         pressed = ""
  28.         ro.drawButton(button)
  29.     end
  30. end
  31.  
  32. -- Processes the graphics for a button being pressed.
  33. local press = function(button)
  34.     unpress(pressed)
  35.     if button ~= "" then
  36.         pressed = button
  37.         ro.drawButton(button)
  38.     end
  39. end
  40.  
  41. -- Checks if a click is within the hitbox of a button.
  42. local checkButtonPress = function(menuPos)
  43.     for a, b in pairs(buttons) do
  44.         local buttonPos = vector.new(menuPos.x-b.x+1, menuPos.y-b.y+1)
  45.         if isBetween(buttonPos.x, 1, a:len(), true) and buttonPos.y == 1 and not disabled[a]() then
  46.             press(a)
  47.             return
  48.         end
  49.     end
  50.     unpress()
  51. end
  52.  
  53. -- Returns the position of the menu.
  54. ro.getPos = function()
  55.     return pos.x, pos.y
  56. end
  57.  
  58. -- Returns the size of the menu.
  59. ro.getSize = function()
  60.     return size.x, size.y
  61. end
  62.  
  63. -- Reposition and/or resize the menu.
  64. ro.reposition = function(x, y, w, h)
  65.   pos.x, pos.y = x or pos.x, y or pos.y
  66.   size.x, size.y = w or size.x, h or size.y
  67.   win.reposition(pos.x, pos.y, size.x, size.y)
  68. end
  69.  
  70. -- Redraws the specified button.
  71. ro.drawButton = function(button)
  72.     if disabled[button]() then
  73.         win.setBackgroundColor(color.disabled[1])
  74.         win.setTextColor(color.disabled[2])
  75.     elseif pressed == button then
  76.         win.setBackgroundColor(color.pressed[1])
  77.         win.setTextColor(color.pressed[2])
  78.     else
  79.         win.setBackgroundColor(color.default[1])
  80.         win.setTextColor(color.default[2])
  81.     end
  82.     if button:sub(1, 10) == "HSeparator" then
  83.         win.setCursorPos(1, buttons[button].y)
  84.         win.write(string.rep("\140", size.x))
  85.     elseif button:sub(1, 10) == "VSeparator" then
  86.         for y = 1, size.y do
  87.             win.setCursorPos(buttons[button].x, y)
  88.             win.write("\149")
  89.         end
  90.     else
  91.         win.setCursorPos(buttons[button].x, buttons[button].y)
  92.         win.write(button)
  93.     end
  94. end
  95.  
  96. -- Redraws the whole menu.
  97. ro.drawMenu = function()
  98.     win.setBackgroundColor(color.default[1])
  99.     win.setTextColor(color.default[2])
  100.     win.clear()
  101.     for a, b in pairs(buttons) do
  102.         ro.drawButton(a)
  103.     end
  104. end
  105.  
  106. -- Processes user input for this menu.
  107. ro.ui = function()
  108.     ro.drawMenu()
  109.     while true do
  110.         local event = events.getEvent()
  111.         if OR(event[1], "mouse_click", "mouse_drag", "mouse_up") and event[2] == 1 then
  112.             local menuPos = vector.new(event[3]-pos.x+1, event[4]-pos.y+1)
  113.             if isBetween(menuPos.x, 1, size.x, true) and isBetween(menuPos.y, 1, size.y, true) then
  114.                 checkButtonPress(menuPos)
  115.                 if event[1] == "mouse_up" and not(pressed == "" or ro[pressed] == nil or disabled[pressed]()) then
  116.                     ro[pressed]()
  117.                 end
  118.             else
  119.                 if event[1] == "mouse_click" then
  120.                     local e, b, x, y = table.unpack(event)
  121.                     if multishell and multishell.getCount() > 1 then
  122.                       y = y + 1
  123.                     end
  124.                     events.queueEvent(e, b, x, y)
  125.                     unpress()
  126.                     return
  127.                 end
  128.             end
  129.             if event[1] == "mouse_up" then
  130.                 unpress()
  131.                 return
  132.             end
  133.  
  134.         elseif event[1] == "key" then
  135.             if OR(event[2], keys.leftAlt, keys.rightAlt) and alt then
  136.                 if pressed == "" then
  137.                     press(alt)
  138.                 else
  139.                     unpress()
  140.                     os.pullEvent("key_up")
  141.                     return
  142.                 end
  143.  
  144.             elseif OR(event[2], keys.right, keys.left, keys.up, keys.down) then
  145.                 if dir[keys.getName(event[2])][pressed] ~= nil then
  146.                     press(dir[keys.getName(event[2])][pressed])
  147.                 end
  148.                 if pressed == "" then press(alt) end
  149.  
  150.             elseif OR(event[2], keys.enter, keys.numPadEnter) then
  151.                 if not(ro[pressed] == nil or disabled[pressed]()) then
  152.                     ro[pressed]()
  153.                 end
  154.                 unpress()
  155.                 break
  156.             end
  157.         end
  158.     end
  159. end
  160.  
  161. return {
  162.     ctor = function(data)
  163.         pos, size = data.pos, data.size
  164.         win = window.create(term.current(), pos.x, pos.y, size.x, size.y)
  165.         alt = data.alt
  166.         color = data.color or {}
  167.         color.default = color.default or {colors.lightGray, colors.black}
  168.         color.pressed = color.pressed or {colors.gray, colors.black}
  169.         color.disabled = colors.disabled or {colors.lightGray, colors.gray}
  170.  
  171.         for a, b in pairs(data.buttons) do
  172.             if ro[a] == nil then ro[a] = b.main
  173.             else error("Cannot use the menu button '" .. a .. "' because it would overwrite an already existing property in this menu.", 2)
  174.             end
  175.             buttons[a] = b.pos
  176.             dir.left[a] = b.left
  177.             dir.right[a] = b.right
  178.             dir.up[a] = b.up
  179.             dir.down[a] = b.down
  180.             if a:sub(2, 10) == "Separator" then disabled[a] = function() return true end
  181.             elseif b.disable == nil then disabled[a] = function() return false end
  182.             else disabled[a] = b.disable end
  183.         end
  184.     end,
  185.     protected = true,
  186.     readOnly = ro
  187. }
  188. end)
  189.  
  190. --[[ Documentation
  191. local exampleMenu = ContextMenu{
  192. • This is how you create a context menu. You can also put the curly brackets in
  193.   parentheses but this way is less cluttery.
  194.  
  195.     pos = vector.new(<x position>, <y position>),
  196.     • This is the coordinates on the screen the menu will be drawn.
  197.     • This is required.
  198.  
  199.     size = vector.new(<width>, <height>),
  200.     • This is the fixed width and height of the menu.
  201.     • This is required.
  202.  
  203.     alt = <button name>,
  204.     • This is the name of the menu button that will be selected first when the
  205.       menu is opened.
  206.     • This is required.
  207.  
  208.     color = {
  209.     • These are the colors the menu and buttons will be depending on if a button
  210.       is pressed or disabled or neither.
  211.     • First value is the background color.
  212.     • Second value is the text color.
  213.     • All colors have a default value so none are required.
  214.  
  215.         default = {<color>, <color>},
  216.         • These are the colors of the whole menu. They are also the colors of
  217.           buttons that are not disabled or pressed.
  218.         • Default background color is 'colors.lightGray'.
  219.         • Default text color is 'colors.black'.
  220.  
  221.         pressed = {<color>, <color>},
  222.         • These are the colors of buttons that are pressed and not disabled.
  223.         • Default background color is 'colors.gray'.
  224.         • Default text color is 'colors.black'.
  225.  
  226.         disabled = {<color>, <color>}
  227.         • These are the colors of buttons that are disabled. These buttons can't
  228.           be pressed.
  229.         • Default background color is 'colors.lightGray'.
  230.         • Default text color is 'colors.gray'.
  231.  
  232.     },
  233.     buttons = {
  234.     • These are the definitions of each button in the menu.
  235.     • This table is required.
  236.     • Technically no buttons are required, but not having at least 1 would make
  237.       the whole menu redundant.
  238.  
  239.         [<button 1 name>] = {
  240.         • The button name is the key/index of the table element.
  241.         • There are some names that your buttons can't have. See below for more
  242.           info.
  243.  
  244.             pos = vector.new(<x>, <y>),
  245.             • This is the coordinates relative to the menu where the button will
  246.               be drawn.
  247.             • This is required.
  248.  
  249.             main = function() ... end,
  250.             • This is the function that runs when the button is pressed.
  251.             • This is not required for separators.
  252.             • This is technically not required for anything else, but not having
  253.               this would make the button redundant.
  254.  
  255.             disable = function() ... end,
  256.             • This function determines if the button should be disabled.
  257.             • This function must return a boolean saying if the button is
  258.               disabled or not.
  259.             • This is NOT required. By default this will return false.
  260.  
  261.             up = <button 2 name>,
  262.             • This is the name of the button that will be selected when the up
  263.               key is pressed.
  264.             • This is NOT required.
  265.  
  266.             down = <button 3 name>,
  267.             • This is the name of the button that will be selected when the down
  268.               key is pressed.
  269.             • This is NOT required.
  270.  
  271.             left = <button 4 name>,
  272.             • This is the name of the button that will be selected when the left
  273.               key is pressed.
  274.             • This is NOT required.
  275.  
  276.             right = <button 5 name>,
  277.             • This is the name of the button that will be selected when the
  278.               right key is pressed.
  279.             • This is NOT required.
  280.  
  281.         },
  282.         [<button 2 name>] = {...},
  283.         • Additional buttons follow the same format as the first one.
  284.  
  285.         ["HSeparator"] = {...},
  286.         • This will add a horizontal line that stretch's from the left to the
  287.           right of the whole menu.
  288.         • This button will always be disabled and will ignore the 'main'
  289.           function.
  290.         • Make sure your separator's name always starts with "HSeparator". The
  291.           name can have any text after that.
  292.         • If you have more than one separator, each one has to have a different
  293.           name, like "HSeparator1", "HSeparator2", etc.
  294.  
  295.         [<button 3 name>] = {...},
  296.         [<button 4 name>] = {...},
  297.         ["VSeparator"] = {...},
  298.         • This follows the same rules as "HSeparator", but it draws a vertical
  299.           line from the top to the bottom of the whole menu instead of a
  300.           horizontal one.
  301.  
  302.         [<button 5 name>] = {...}
  303.     }
  304. }
  305.  
  306. The following methods are used in the object and therefore are protected. Avoid
  307. naming your buttons the same as any of these methods. Methods are case sensitive
  308. so your button may be the same name as long as capitalization is different.
  309.  
  310. exampleMenu.getPos()
  311. • This returns the x and y coordinates of the menu.
  312.  
  313. exampleMenu.getSize()
  314. • This returns the width and height of the menu.
  315.  
  316. exampleMenu.reposition(x, y, w, h)
  317. • This changes the current position and/or size of the menu.
  318.  
  319. exampleMenu.drawMenu()
  320. • This redraws the whole menu.
  321.  
  322. exampleMenu.drawButton()
  323. • This redraws a specific menu button.
  324.  
  325. exampleMenu.ui()
  326. • This enables the menu UI.
  327. ]]
  328.  
  329. --[[ Changelog
  330. 2025/04/09:
  331. • Added a 'reposition' method. This was added for the when the screen size
  332.   changes and the menu has to be resized or repositioned as a result. Obviously
  333.   it does not need to be used exclusively for that reason.
  334. • The 'queueEvent' bug addressed in the last update is still apparently a
  335.   problem. It still causes the tab above the clicked menu button to be clicked
  336.   at the same time as the menu button. Clicking the button functions as expected
  337.   anyway, so it's just an inconvenience. This likely can't get fixed unless the
  338.   CC devs change how 'queueEvent' is handled with multishell.
  339.  
  340. 2024/09/14:
  341. • Fixed a bug in the ui method caused by multishell when os.queueEvent is
  342.   called. With mouse events, clicking on coordinate (1,2) will translate to
  343.   (1,1) for the running program. Queuing the same event at (1,1) will not
  344.   translate and will instead click on the multishell tab at that position of
  345.   the screen.
  346. • There is now a check for if the current computer is an advanced computer by
  347.   checking if multishell exists. If it does, there will then be a check if
  348.   there is more than 1 tab open in multishell. If there is, the y position
  349.   of the mouse event will be moved down by 1.
  350.  
  351. 2024/01/02:
  352. • Changed all instances of 'vector2' to 'vector' to comply with the changes made
  353.   to 'LibAppend.lua'.
  354. • Removed some commented debugging code.
  355. • Fixed some typos in the documentation.
  356. • Rewrote some parts of the documentation to be more descriptive.
  357. • Changed the color parameter to use a basic array instead of a vector (should
  358.   reduce memory usage).
  359. • Rewrote the 'drawButton' method to flow better. Fixed the vertical separator
  360.   not displaying correctly. Fixed the separators not starting at the top/left
  361.   edges of the menu.
  362. • Removed the 'disable' table because it was unused.
  363. • Replaced/added various comments with more descriptive text.
  364.  
  365. 2023/03/09:
  366. • Completely rewrote this program to comply with the changes made to 'Class.lua'
  367.   and 'Events.lua'.
  368. • Removed the automatic download of required API files. The files must be
  369.   downloaded manually.
  370. • Paste exposure has been returned to public.
  371.  
  372. 2023/02/23:
  373. • Changed require directories from "/API/Raiu/" to "/API/".
  374. • Renamed vector to vector2 to reflect the changes to "/API/LibAppend.lua".
  375. • Reformatted the changelog so it's a bit cleaner than before.
  376. • Changed to unlisted until I can fix the changes made to "/API/events.lua".
  377.  
  378. 2022/07/20:
  379. • Added an example for how to use the menuClass.
  380.  
  381. 2022/07/15:
  382. • Added parameters for custom menu colors.
  383.  
  384. 2022/07/13:
  385. • Added require functions for required APIs so the main program doesn't have to.
  386.  
  387. 2022/06/29:
  388. • Updated eventListener() to events.listen().
  389.  
  390. 2022/06/28:
  391. • Completely rewrote the ui and graphical code (twice). Old code was a complete
  392.   mess and unintuitive.
  393. • With the new code, everything is handled within the context menu class.
  394. • Now works with both mouse clicks and key presses.
  395. ]]
Advertisement
Add Comment
Please, Sign In to add comment