├── .gitattributes ├── .gitignore ├── README.md ├── docs ├── API.md ├── example-container.lua ├── example.lua └── example.smartfs ├── init.lua ├── mod.conf └── smartfs.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | smartfs 2 | ======= 3 | 4 | This mod provides a 2nd generation way of creating forms - this means that the modder does not need to worry about complex formspec strings 5 | 6 | * Expandable: you can register your own elements to use on the form. 7 | * Easy event handling: use binding to handle events. 8 | * New elements: Includes a toggle button 9 | 10 | License: WTFPL 11 | 12 | # Using Smart Formspec 13 | Smartfs provides 2nd generation Minetest forms to replace clunky formspec strings. Each smartfs form is a container filled with GUI elements. A number of default elements are included with smartfs, but modders can also define their own custom elements. This document describes the basic usage of the smartfs API. 14 | 15 | ## Installation 16 | Smartfs can be used as a library or a mod. 17 | 18 | To use smartfs as a library, copy the smartfs.lua file to your mod folder and add 19 | `local smartfs = dofile(minetest.get_modpath(minetest.get_current_modname()).."/smartfs.lua")` 20 | 21 | to the top of your init.lua. If your mod is splitted to multiple files you can transport the library reference trough your mod namespace 22 | `yourmod.smartfs = dofile(minetest.get_modpath(minetest.get_current_modname()).."/smartfs.lua")` 23 | 24 | To use smartfs as a mod, add it to your game's mods folder or to the user mods folder and enable it. 25 | You need to set up a dependency for your mod to use it. The library is available in the global "smartfs" table in this case. 26 | 27 | ## Creating and showing forms 28 | A form is a rectangular area of the screen upon which all elements are placed. Use the smartfs.create() function to create a new form. This function takes two arguments and returns a form object. 29 | 30 | The first argument is a unique string that identifies the form. The second argument is a function that should take a single argument called state which is used to set form properties like size and background color. State also has constructors for all form elements and can be used with state:element_name. Below is a quick example. 31 | 32 | myform = smartfs.create("My Form",function(state) 33 | --sets the form's size 34 | -- (width, hieght) 35 | state:size(5,5) 36 | 37 | --creates a label and places it on the form 38 | --(x-pos, y-pos, name, text) 39 | state:label(3,3,"label1", "A label!") 40 | end) 41 | 42 | Forms can be shown to the player by using the show(target) function. The target argument is the name of the player that will see the form. 43 | 44 | myform:show("singleplayer") 45 | 46 | Here is a list of steps the library takes. 47 | * You create a new form using smartfs.create(). 48 | * The form is registered and stored for later use. 49 | * You show a form to a player using the myform:show() 50 | * The state is created and stored. 51 | * The function in smartfs.create runs and creates the elements. 52 | * The form is displayed to the player. 53 | 54 | ## Modifying Elements 55 | Elements have functions of the form element:function(args) where you need to have access to the element object. 56 | 57 | You can get the element object by assigning a variable to its creation function like so: 58 | 59 | local button1 = state:button(0,0, 1,4, "btn1", "A button") 60 | --button1 is now a table representing the button 61 | 62 | You can also get the element by using state:get(name). The example below will retrieve a button with the name "btn1": 63 | 64 | button1 = state:get("btn1") 65 | --or 66 | state:get("btn1"):onClick(your_onclick_function) 67 | 68 | Both of these methods should be used inside the form creation callback function, the function you pass to smartfs.create, or in event callbacks. 69 | 70 | Now that you have located your element you can modify it. 71 | 72 | button1:setPos(4,0) 73 | 74 | ## Inventory Support 75 | Smartfs supports adding a button to Sfinv, Inventory+, or Unified Inventory which will open one of your own custom forms. Use the `smartfs.add_to_inventory(form, icon, title)` function where form is the smartfs form linked to by the button, icon is the button image (only for unified inventory), title is the button text (for inventory+ and sfinv), and show_inv specifies whether to include the player inventory by default (for unified inventory and sfinv). 76 | 77 | smartfs.add_to_inventory(form, icon, title, show_inv) 78 | 79 | ## Dynamic forms 80 | Dynamic forms allow you to make a form without having to register it before the game finished loading. 81 | 82 | local state = smartfs.dynamic("smartfs:dyn_form", name) 83 | state:load(minetest.get_modpath("smartfs").."/example.smartfs") 84 | state:get("btn"):click(function(self,state) 85 | print("Button clicked!") 86 | end) 87 | state:show() 88 | 89 | Make sure you call state:show() 90 | 91 | 92 | ## Formspec on nodes 93 | SmartFS is able to attach a formspec to a node metadata. The attached form can be opened by any player and all players see the same. If a player enter information and take updates on the form, all users see the updated form instantly. All changes are saved in node meta data and visible to all players. 94 | Do not write sensitive to the formspec since the nodemeta is sent to all player clients. To show something sensitive to only a single player info please use form:show method instead of nodemeta. 95 | Please note: there is a "reset" implemented if all players leave the node formspec, the initial state is restored. 96 | 97 | minetest.register_node("smartfs:demoblock", { 98 | description = "SmartFS Demo block", 99 | groups = {cracky = 3}, 100 | tiles = {"demo.png"}, 101 | after_place_node = function(pos, placer, itemstack, pointed_thing) 102 | state:attach_nodemeta(pos, placer) 103 | end, 104 | on_receive_fields = smartfs.nodemeta_on_receive_fields 105 | }) 106 | 107 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # Full API 2 | ## Smartfs 3 | * smartfs( name ) - returns the form regisered with the name 'name' 4 | * smartfs.create( name,function ) - creates a new form and adds elements to it by running the function. Use before Minetest loads. (like minetest.register_node) 5 | * smartfs.element( name, data ) - creates a new element type. 6 | * smartfs.dynamic( formname, playername ) - creates a dynamic form. Returns state. See example.lua for example. Remember to call state:show() 7 | * smartfs.add_to_inventory(form, icon, title, show_inv) - Adds a form to an installed advanced inventory. Returns true on success. 8 | * smartfs.set_player_inventory(form) - Set the form as players main inventory for all player 9 | * smartfs.inventory_mod() - Returns the name of an installed and supported inventory mod that will be used above, or nil. 10 | * smartfs.override_load_checks() - Allows you to use smartfs.create after the game loads. Not recommended! 11 | * smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender) - on_receive_fields callback can be used in minetest.register_node for nodemeta forms 12 | 13 | ## Form 14 | * form:show( playername [, parameters] ) - shows the form to a player. See state.param. 15 | * form.name - the name of the form. 16 | * form:attach_to_node(nodepos, params) - Attach a form to a node meta (usable in register_node's constructor, on_placenode, or dynamically) 17 | 18 | ## Supported locations 19 | * unified_inventory, inventory_plus, or sfinv plugins - assigned by smartfs.add_to_inventory() - auto-detection which inventory should be used 20 | * player / show_formspec() - used for form:show(player) 21 | * (main) inventory - assigned by smartfs.set_player_inventory() 22 | * nodemeta - assigned by form:attach_to_node(nodepos, params) 23 | * container - used internally for state:container() 24 | 25 | ## State 26 | 27 | ### Methods 28 | * state:size( width,height ) - sets the forms width and height. 29 | * state:get( name ) - gets an element by name. 30 | * state:show() - reshows the form to the player. 31 | * state:close() - closes the form (does not work yet, due to no MT api support). 32 | * state:load( filepath ) - Loads elements from a file. 33 | * state:save( filepath ) - Saves elements to a file. 34 | * state:onInput( function(self,fields,playername)) - specify a function to run after data and/or events received 35 | * state:button( x,y,w,h,name,text [, exit_on_click] ) - create a new button at x,y with name and caption (text) 36 | * ^ optional: exit_on_click - set to true to exit the form when the button is clicked. ( Also see button.setClose() ) 37 | * state:image_button( x,y,w,h,name,text, image [, exit_on_click] ) create a new button with image. 38 | * state:item_image_button( x,y,w,h,name,text, item [, exit_on_click] ) create a new button with item as image. 39 | * state:toggle( x,y,w,h,name,list ) - create a new toggle button at x,y with name and possible list of values 40 | * state:label( x,y,name,text ) - create a new label at x,y with name and caption (text) 41 | * state:vertlabel( x,y,name,text ) - create a new vertical label at x,y with name and caption (text) 42 | * state:field( x,y,w,h,name,label ) - create a new field at x,y with label 43 | * state:pwdfield( x,y,w,h,name,label ) - create a password field 44 | * state:textarea( x,y,w,h,name,label ) - create a new textarea 45 | * state:image( x,y,w,h,name,image ) - create an image box 46 | * state:background( x,y,w,h,name,image ) - create an image box in background 47 | * state:item_image( x,y,w,h,name,itemname ) - create an item image box 48 | * state:inventory( x,y,w,h,name ) - create an inventory listing (use 'main' as name for the main player inventory) 49 | * state:checkbox( x,y,name,label,selected ) - create a check box. 50 | * state:listbox( x,y,w,h,name,selected, transparent ) - create a list box 51 | * state:dropdown( x,y,w,h,name,selected ) - create a drop down list 52 | * state:container(x,y,name) - Add a container with elements shift relative to x,y 53 | * state:view(x,y,name) - Add a virtual container (view). element coordinates are ablsolute to the parent view 54 | * state:element( element_type, data ) - Semi-private, create an element with type and data. 55 | 56 | ### Variables 57 | * state.players - Object to handle players connected to the formspec 58 | * state.players:connect(playername) - register player is viewing the formspec (should be used only by framework) 59 | * state.players:disconnect(playername) - remove player from active viewers (should be used only by framework) 60 | * state.players:get_first() - get the first player, or nil if no players connected 61 | * state.param - The parameters supplied by form:show. 62 | * state.def - The form definition. 63 | * state.location - defines the location the form is assigned - please use it read only 64 | * state.location.type - defines the assignment type. Values: "player", "inventory", "nodemeta" 65 | * state.location.player - the assigned player ("player" and "inventory" only) 66 | * state.location.pos - the assigned node position ("nodemeta" only) 67 | 68 | ## State Elements 69 | 70 | ### All elements / abstract 71 | * element:setPosition( x,y ) - change the position 72 | * element:getPosition() - get the current position 73 | * element:setSize( w,h ) - set the size 74 | * element:getSize() - get the size 75 | * element:setBackground(image) - Set the background of element. Please note a size needs to be defined on element 76 | * element:getBackground() - get the current background 77 | * element:setVisible(bool) - set the visibility status (set hidden=>false, unhide=>true or nil) 78 | * element:getVisible() - get the visibility status 79 | * element:setValue(string) - set value for the element, called internally from on_receive_fields 80 | * element:setTooltip(text) - set the tooltip for the button 81 | * element:getTooltip() - get the current tooltip 82 | 83 | ### Button 84 | * element:setText( text ) - set the caption of the button 85 | * element:getText() - get the caption of the button 86 | * element:setImage( filename ) - sets the background of the button 87 | * element:getImage() - get the background filename of the button 88 | * element:setItem(item) - Set a registred itemname and convert the button to item_image_button 89 | * element:getItem() - get the current itemname 90 | * element:setClose(bool) - set option (bool) if the button should close the form 91 | * element:getClose() - get the current close setting 92 | * element:click( func(self,state,playername) ) - specify a function to run when the button is clicked (equal to onClick) 93 | 94 | ### Toggle Button 95 | * element:getText() - get the text of the toggle option 96 | * element:setId( filename ) - sets the selected id 97 | * element:getId() - get the selected id 98 | * element:onToggle( func(self,state,playername) ) - specify a function to run when the value if toggled 99 | 100 | ### Label 101 | * element:setText( text ) - set the caption of the label 102 | * element:getText() - get the caption of the label 103 | 104 | ### Image and Background 105 | * element:setImage( image ) - set image 106 | * element:getImage() - get the image 107 | 108 | ### Checkbox 109 | * element:setValue( bool ) - set the value 110 | * element:getValue() - get the value 111 | * element:onToggle( func(self,state,playername) ) - specify a function to run when the value if toggled 112 | 113 | ### Field and Text Area 114 | * element:setText( text ) - set the caption of the button 115 | * element:getText() - get the caption of the field 116 | * element:isPassword() - returns true if the field is a password field 117 | * element:isMultiline() - returns true if the field is a miltiline textarea 118 | * element:setCloseOnEnter(bool) - Set the field_close_on_enter string, usually set to false to disable the formspec close on enter 119 | * element:getCloseOnEnter() - Get the field_close_on_enter value 120 | * element:onKeyEnter(func(self, state, playername) - process the Enter key action for this fiels 121 | 122 | ### List box 123 | * element:onClick( func(self,state,idx,playername) ) - function to run when listbox item idx is clicked 124 | * element:onDoubleClick( func(self,state,idx,playername) ) - function to run when listbox item idx is double clicked 125 | * element:addItem( item ) - appends and item - returns the index for the item 126 | * element:removeItem( idx ) - remove item 127 | * element:getItem( idx ) - get Item idx 128 | * element:popItem() - removes last item and returns 129 | * element:clearItems() - empty the list 130 | * element:setSelected( idx ) - set item selection to idx 131 | * element:getSelected() - get selected item (index) 132 | * element:getSelectedItem() - get selected item (value) 133 | 134 | ### Drop Down list 135 | * element:onSelect( func(self,state,field,playername) ) - function to run when dropdown entry selected 136 | * element:addItem( item ) - appends and item 137 | * element:removeItem( idx ) - remove item 138 | * element:getItem( idx ) - get Item idx 139 | * element:popItem() - removes last item and returns 140 | * element:clearItems() - empty the dropdown list 141 | * element:setSelected( idx ) - set item selection to idx 142 | * element:getSelected() - get selected item (index) 143 | * element:getSelectedItem() - get selected item (value) 144 | 145 | ### Inventory listing 146 | * element:setLocation( location ) - set a custom inventory location or nil for the default (current_player) 147 | * element:usePosition( position ) - use a node metadata attached inventory of the node at the given positon 148 | * element:useDetached( name ) - use a detached inventory with the given name 149 | * element:usePlayer( name ) - use a player inventory other than the current player 150 | * element:getLocation() - returns the inventory location (default: current_player) 151 | * element:setList( list ) - set a custom inventory list name or nil for the default (the element's name) 152 | * element:getList() - returns the list name (defaults to the element's name) 153 | * element:setIndex( index ) - set the inventory starting index 154 | * element:getIndex() - returns the inventory starting index 155 | 156 | ### Custom Code 157 | * element:onSubmit( func(self) ) - on form submit 158 | * element:onBuild( func(self) ) - run every time form is shown. You can set code from here 159 | * element:setCode( code ) - set the formspec code 160 | * element:getCode( code ) - get the formspec code 161 | 162 | ### Container/View 163 | * element:getContainerState() - returns the container's sub-state to work with or add container elements 164 | -------------------------------------------------------------------------------- /docs/example-container.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------- 2 | --- Exemple nested container 3 | ----------------------------------------------------------------- 4 | 5 | local header_form_function = function(state) 6 | local label = state:label(0,0,"lbl","SmartFS formspec using nested containers") 7 | label:setSize(10,0.5) 8 | state:toggle(0,0.5,3,1,"img_tog",{"img on","img off"}):onToggle(function(self,func) 9 | if state:get("img"):getVisible() then 10 | state:get("img"):setVisible(false) 11 | else 12 | state:get("img"):setVisible(true) 13 | end 14 | end) 15 | local tog1 = state:toggle(3,0.5,3,1,"container1_tog",{"container1 on","container1 off"}) 16 | tog1:onToggle(function(self,func) 17 | if state:get("container1"):getVisible() then 18 | state:get("container1"):setVisible(false) 19 | else 20 | state:get("container1"):setVisible() 21 | end 22 | end) 23 | state:toggle(6,0.5,3,1,"container2_tog",{"container2 on","container2 off"}):onToggle(function(self,func) 24 | local container2 = state:get("container1"):getContainerState():get("container2") 25 | if container2:getVisible() then 26 | container2:setVisible(false) 27 | else 28 | container2:setVisible() 29 | end 30 | end) 31 | end 32 | 33 | local main_form_function = function(state) 34 | -- load the header inplace to the current state 35 | state:size(10,10) 36 | header_form_function(state) 37 | local image = state:image(0,2,1,1,"img","default_ladder_steel.png") 38 | state:toggle(0,3,3,1,"tg",{"plenty..","of..","custom..","elements"}) 39 | state:onInput(function(state, fields, player) 40 | print("hook 1") 41 | end) 42 | 43 | local container1_element = state:view(0, 4, "container1") 44 | container1_element:setBackground("default_stone.png") 45 | local sub_state1 = container1_element:getContainerState() 46 | sub_state1:size(10,6) 47 | -- all x/y values needs to be "real" 48 | sub_state1:image(0,4,1,1,"img","default_ladder_wood.png") 49 | sub_state1:toggle(0,5,3,1,"tg",{"plenty..","of..","custom..","elements"}) 50 | sub_state1:onInput(function(state, fields, player) 51 | print("sub_state1 hook 2") 52 | end) 53 | 54 | local container2_element = sub_state1:container(0, 7, "container2") --nested to sub_state1 55 | 56 | container2_element:setBackground("default_brick.png") 57 | local sub_state2 = container2_element:getContainerState() 58 | sub_state2:size(10,3) 59 | -- all the x/y values in relation to container 60 | sub_state2:image(0,0,1,1,"img", "default_stone.png") 61 | local tog2 = sub_state2:toggle(0,1,3,1,"tg",{"second..","toggle..","with..","same..","name.."}) 62 | tog2:setBackground("default_gold_block.png") 63 | sub_state2:onInput(function(state, fields, player) 64 | print("sub_state2 hook 1") 65 | end) 66 | end 67 | 68 | 69 | local form_container = smartfs.create("smartfs:container_form",main_form_function) 70 | 71 | minetest.register_chatcommand("sfs_v", { 72 | params = "", 73 | description = "SmartFS test formspec with nested containers", 74 | func = function(name, param) 75 | form_container:show(name) 76 | end, 77 | }) 78 | 79 | 80 | ----------------------------------------------------------------- 81 | --- Exemple tabbed container 82 | ----------------------------------------------------------------- 83 | local tab1_def = function(state) 84 | state:label(0,0,"lbl","Tab1 Content") 85 | state:toggle(0,1,2,2,"tog",{"plenty..","of..","custom..","elements"}) 86 | end 87 | 88 | local tab2_def = function(state) 89 | state:label(0,0,"lbl","Tab2 Content") 90 | state:field(0.5,2,4,1,"txt","Textbox") 91 | local listbox = state:listbox(0,3,9,5,"list") 92 | listbox:addItem("First entry") 93 | listbox:addItem("Second entry") 94 | end 95 | 96 | local tab3_def = function(state) 97 | state:label(0,0,"lbl","Tab3 Content") 98 | local area = state:textarea(0.5,1,9,5,"ta","Text:") 99 | area:setText("Hallo") 100 | end 101 | 102 | 103 | local tab_form_function = function(state) 104 | state:size(10,10) 105 | local tab_element = state:container(0, 0, "tab_container") 106 | local tab_state = tab_element:getContainerState() 107 | tab_state:size(10,10) 108 | 109 | local tab_controller = { 110 | _tabs = {}, 111 | set_active = function(self, tabname) 112 | for name, def in pairs(self._tabs) do 113 | if name == tabname then 114 | def.button:setBackground("default_gold_block.png") 115 | def.container:setVisible() 116 | else 117 | def.button:setBackground(nil) 118 | def.container:setVisible(false) 119 | end 120 | end 121 | end, 122 | tab_add = function(self, name, def) 123 | self._tabs[name] = def 124 | end, 125 | } 126 | 127 | local tab1 = {} 128 | tab1.button = tab_state:button(0,0,2,1,"tab1_btn","Tab 1") 129 | tab1.button:onClick(function(self) 130 | tab_controller:set_active("tab1") 131 | end) 132 | tab1.container = tab_state:container(0,1,"tab1_container") 133 | tab1.containerstate = tab1.container:getContainerState() 134 | tab1_def(tab1.containerstate) 135 | tab1.containerstate:size(10,9) 136 | tab_controller:tab_add("tab1", tab1) 137 | 138 | local tab2 = {} 139 | tab2.button = tab_state:button(2,0,2,1,"tab2_btn","Tab 2") 140 | tab2.button:onClick(function(self) 141 | tab_controller:set_active("tab2") 142 | end) 143 | tab2.container = tab_state:container(0,1,"tab2_container") 144 | tab2.containerstate = tab2.container:getContainerState() 145 | tab2_def(tab2.containerstate) 146 | tab2.containerstate:size(10,9) 147 | tab_controller:tab_add("tab2", tab2) 148 | 149 | local tab3 = {} 150 | tab3.button = tab_state:button(4,0,2,1,"tab3_btn","Tab 3") 151 | tab3.button:onClick(function(self) 152 | tab_controller:set_active("tab3") 153 | end) 154 | tab3.container = tab_state:container(0,1,"tab3_container") 155 | tab3.containerstate = tab3.container:getContainerState() 156 | tab3_def(tab3.containerstate) 157 | tab3.containerstate:size(10,9) 158 | tab_controller:tab_add("tab3", tab3) 159 | 160 | tab_controller:set_active("tab1") --default tab 161 | end 162 | 163 | 164 | local form_tab = smartfs.create("smartfs:container_tab", tab_form_function) 165 | minetest.register_chatcommand("sfs_t", { 166 | params = "", 167 | description = "SmartFS test formspec with tabbed containers", 168 | func = function(name, param) 169 | form_tab:show(name) 170 | end, 171 | }) 172 | 173 | --smartfs.set_player_inventory(form_tab) 174 | -------------------------------------------------------------------------------- /docs/example.lua: -------------------------------------------------------------------------------- 1 | 2 | local s = smartfs.create("smartfs:form", function(state) 3 | state:size(10,7) 4 | state:label(2,0,"lbl","SmartFS example formspec!") 5 | local usr = state:label(7,0,"user","") 6 | if state.location.type ~= "nodemeta" then -- display location user name if it is user or inventory formspec 7 | usr:setText(state.location.player) 8 | end 9 | usr:setSize(3,0.5) 10 | usr:setBackground("halo.png") 11 | local textbox = state:field(7.25,1.25,3,1,"txt","Textbox") 12 | textbox:setCloseOnEnter(false) 13 | textbox:onKeyEnter(function(self, state, playername) 14 | print("Enter pressed in Textbox field") 15 | end) 16 | 17 | state:image(0,0,2,2,"img","default_stone.png") 18 | local toggle = state:toggle(0,2,3,1,"tg",{"plenty..","of..","custom..","elements"}) 19 | toggle:onToggle(function(self, state, player) 20 | if state:get("ta"):getVisible() == false then 21 | state:get("ta"):setVisible() 22 | else 23 | state:get("ta"):setVisible(false) 24 | end 25 | end) 26 | 27 | state:checkbox(2,1,"c","Easy code",true) 28 | state:vertlabel(0,3.5,"vlbl","Example!") 29 | local area = state:textarea(1,3.5,9,4,"ta","Code:") 30 | local res = [[ 31 | smartfs.create("smartfs:form",function(state) 32 | state:size(10,7) 33 | state:label(2,0,"lbl","SmartFS example formspec!") 34 | state:field(7,1,3,1,"txt","Textbox") 35 | state:image(0,0,2,2,"img","default_stone.png") 36 | state:toggle(0,2,3,1,"tg",{"plenty..","of..","custom..","elements"}) 37 | state:checkbox(2,1,"c","Easy code",true) 38 | end)]] 39 | 40 | area:setText(res) 41 | 42 | state:onInput(function(self, fields, user) -- processed on any (supported) input 43 | if state.location.type == "nodemeta" then 44 | usr:setText(user) -- display current user who sent the data 45 | end 46 | end) 47 | return true 48 | end) 49 | 50 | local l = smartfs.create("smartfs:load", function(state) 51 | state:load(minetest.get_modpath("smartfs").."/docs/example.smartfs") 52 | state:get("btn"):click(function(self,state) 53 | print("Button clicked!") 54 | end) 55 | return true 56 | end) 57 | 58 | smartfs.add_to_inventory(l,"icon.png","SmartFS") 59 | 60 | minetest.register_chatcommand("sfs_s", { 61 | params = "", 62 | description = "SmartFS test formspec 1: basics", 63 | func = function(name, param) 64 | s:show(name) 65 | end, 66 | }) 67 | minetest.register_chatcommand("sfs_l", { 68 | params = "", 69 | description = "SmartFS test formspec 2: loading", 70 | func = function(name, param) 71 | l:show(name) 72 | end, 73 | }) 74 | 75 | minetest.register_chatcommand("sfs_d", { 76 | params = "", 77 | description = "SmartFS test formspec 3: dynamic", 78 | func = function(name, param) 79 | local state = smartfs.dynamic("smartfs:dyn_form", name) 80 | state:load(minetest.get_modpath("smartfs").."/docs/example.smartfs") 81 | state:get("btn"):click(function(self,state) 82 | print("Button clicked!") 83 | end) 84 | state:show() 85 | end, 86 | }) 87 | 88 | minetest.register_chatcommand("sfs_lc", { 89 | params = "", 90 | description = "SmartFS test formspec 4: smartfs.create error catching", 91 | func = function(name, param) 92 | smartfs.create("asdinas", function() end) 93 | end 94 | }) 95 | 96 | minetest.register_node("smartfs:demoblock", { 97 | description = "SmartFS Demo block", 98 | groups = {cracky = 3}, 99 | tiles = {"demo.png"}, 100 | after_place_node = function(pos, placer, itemstack, pointed_thing) 101 | s:attach_to_node(pos, placer) 102 | end, 103 | on_receive_fields = smartfs.nodemeta_on_receive_fields 104 | }) 105 | -------------------------------------------------------------------------------- /docs/example.smartfs: -------------------------------------------------------------------------------- 1 | return { ["ele"] = { ["c"] = { ["pos"] = { ["y"] = 1, ["x"] = 1 }, ["label"] = "Check", ["value"] = true, ["type"] = "checkbox", ["name"] = "c" }, ["btn"] = { ["pos"] = { ["y"] = 2, ["x"] = 1 }, ["size"] = { ["h"] = 1, ["w"] = 1 }, ["value"] = "Button", ["type"] = "button", ["name"] = "btn" } }, ["size"] = { ["h"] = 3, ["w"] = 5 } } -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | smartfs = dofile(minetest.get_modpath("smartfs").."/smartfs.lua") 2 | -- dofile(minetest.get_modpath("smartfs").."/docs/example.lua") 3 | -- dofile(minetest.get_modpath("smartfs").."/docs/example-container.lua") 4 | 5 | assert(minetest.is_yes(true) == true) 6 | assert(minetest.is_yes(false) == false) 7 | -------------------------------------------------------------------------------- /mod.conf: -------------------------------------------------------------------------------- 1 | name = smartfs 2 | title = Smart Formspecs Library 3 | author = rubenwardy 4 | optional_depends = unified_inventory, inventory_plus, sfinv 5 | description = A library to allow mods to make Formspecs, a form of GUI, easily. 6 | license = CC0 7 | forum = https://forum.minetest.net/viewtopic.php?t=7553 8 | version = 1.1.0 9 | -------------------------------------------------------------------------------- /smartfs.lua: -------------------------------------------------------------------------------- 1 | --------------------------- 2 | -- SmartFS: Smart Formspecs 3 | -- License: CC0 or WTFPL 4 | -- by Rubenwardy 5 | --------------------------- 6 | 7 | local smartfs = { 8 | _fdef = {}, 9 | _edef = {}, 10 | _ldef = {}, 11 | opened = {}, 12 | inv = {} 13 | } 14 | 15 | local function boolToStr(v) 16 | return v and "true" or "false" 17 | end 18 | 19 | -- the smartfs() function 20 | function smartfs.__call(self, name) 21 | return smartfs.get(name) 22 | end 23 | 24 | function smartfs.get(name) 25 | return smartfs._fdef[name] 26 | end 27 | 28 | ------------------------------------------------------ 29 | -- Smartfs Interface - Creates a new form and adds elements to it by running the function. Use before Minetest loads. (like minetest.register_node) 30 | ------------------------------------------------------ 31 | -- Register forms and elements 32 | function smartfs.create(name, onload) 33 | assert(not smartfs._fdef[name], 34 | "SmartFS - (Error) Form "..name.." already exists!") 35 | assert(not smartfs.loaded or smartfs._loaded_override, 36 | "SmartFS - (Error) Forms should be declared while the game loads.") 37 | 38 | smartfs._fdef[name] = { 39 | form_setup_callback = onload, 40 | name = name, 41 | show = smartfs._show_, 42 | attach_to_node = smartfs._attach_to_node_ 43 | } 44 | 45 | return smartfs._fdef[name] 46 | end 47 | 48 | ------------------------------------------------------ 49 | -- Smartfs Interface - Creates a new element type 50 | ------------------------------------------------------ 51 | function smartfs.element(name, data) 52 | assert(not smartfs._edef[name], 53 | "SmartFS - (Error) Element type "..name.." already exists!") 54 | 55 | assert(data.onCreate, "element requires onCreate method") 56 | smartfs._edef[name] = data 57 | return smartfs._edef[name] 58 | end 59 | 60 | ------------------------------------------------------ 61 | -- Smartfs Interface - Creates a dynamic form. Returns state 62 | ------------------------------------------------------ 63 | function smartfs.dynamic(name,player) 64 | if not smartfs._dynamic_warned then 65 | smartfs._dynamic_warned = true 66 | minetest.log("warning", "SmartFS - (Warning) On the fly forms are being used. May cause bad things to happen") 67 | end 68 | local statelocation = smartfs._ldef.player._make_state_location_(player) 69 | local state = smartfs._makeState_({name=name}, nil, statelocation, player) 70 | smartfs.opened[player] = state 71 | return state 72 | end 73 | 74 | ------------------------------------------------------ 75 | -- Smartfs Interface - Returns the name of an installed and supported inventory mod that will be used above, or nil 76 | ------------------------------------------------------ 77 | function smartfs.inventory_mod() 78 | if minetest.global_exists("unified_inventory") then 79 | return "unified_inventory" 80 | elseif minetest.global_exists("inventory_plus") then 81 | return "inventory_plus" 82 | elseif minetest.global_exists("sfinv") then 83 | return "sfinv" 84 | else 85 | return nil 86 | end 87 | end 88 | 89 | ------------------------------------------------------ 90 | -- Smartfs Interface - Adds a form to an installed advanced inventory. Returns true on success. 91 | ------------------------------------------------------ 92 | function smartfs.add_to_inventory(form, icon, title, show_inv) 93 | local ldef 94 | local invmod = smartfs.inventory_mod() 95 | if invmod then 96 | ldef = smartfs._ldef[invmod] 97 | else 98 | return false 99 | end 100 | return ldef.add_to_inventory(form, icon, title, (show_inv == nil) and true or show_inv) 101 | end 102 | 103 | ------------------------------------------------------ 104 | -- Smartfs Interface - Set the form as players inventory 105 | ------------------------------------------------------ 106 | function smartfs.set_player_inventory(form) 107 | smartfs._ldef.inventory.set_inventory(form) 108 | end 109 | ------------------------------------------------------ 110 | -- Smartfs Interface - Allows you to use smartfs.create after the game loads. Not recommended! 111 | ------------------------------------------------------ 112 | function smartfs.override_load_checks() 113 | smartfs._loaded_override = true 114 | end 115 | 116 | ------------------------------------------------------ 117 | -- Smartfs formspec locations 118 | ------------------------------------------------------ 119 | -- Unified inventory plugin 120 | smartfs._ldef.unified_inventory = { 121 | add_to_inventory = function(form, icon, title, show_inv) 122 | unified_inventory.register_button(form.name, { 123 | type = "image", 124 | image = icon, 125 | }) 126 | unified_inventory.register_page(form.name, { 127 | get_formspec = function(player, formspec) 128 | local name = player:get_player_name() 129 | local state 130 | if smartfs.inv[name] and smartfs.inv[name].def.name == form.name then 131 | state = smartfs.inv[name] 132 | else 133 | local statelocation = smartfs._ldef.unified_inventory._make_state_location_(name) 134 | state = smartfs._makeState_(form, nil, statelocation, name) 135 | if form.form_setup_callback(state) ~= false then 136 | smartfs.inv[name] = state 137 | else 138 | smartfs.inv[name] = nil 139 | return "" 140 | end 141 | end 142 | return {formspec = state:_buildFormspec_(false), draw_inventory = show_inv} 143 | end 144 | }) 145 | end, 146 | _make_state_location_ = function(player) 147 | return { 148 | type = "inventory", 149 | player = player, 150 | _show_ = function(state) 151 | unified_inventory.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state.def.name) 152 | end 153 | } 154 | end 155 | } 156 | 157 | -- Inventory plus plugin 158 | smartfs._ldef.inventory_plus = { 159 | add_to_inventory = function(form, icon, title) 160 | minetest.register_on_joinplayer(function(player) 161 | inventory_plus.register_button(player, form.name, title) 162 | end) 163 | minetest.register_on_player_receive_fields(function(player, formname, fields) 164 | if formname == "" and fields[form.name] then 165 | local name = player:get_player_name() 166 | local statelocation = smartfs._ldef.inventory_plus._make_state_location_(name) 167 | local state = smartfs._makeState_(form, nil, statelocation, name) 168 | if form.form_setup_callback(state) ~= false then 169 | smartfs.inv[name] = state 170 | state:show() 171 | end 172 | end 173 | end) 174 | end, 175 | _make_state_location_ = function(player) 176 | return { 177 | type = "inventory", 178 | player = player, 179 | _show_ = function(state) 180 | inventory_plus.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state:_buildFormspec_(true)) 181 | end 182 | } 183 | end 184 | } 185 | 186 | -- Sfinv plugin 187 | smartfs._ldef.sfinv = { 188 | add_to_inventory = function(form, icon, title, show_inv) 189 | sfinv.register_page(form.name, { 190 | title = title, 191 | get = function(self, player, context) 192 | local name = player:get_player_name() 193 | local state 194 | if smartfs.inv[name] then 195 | state = smartfs.inv[name] 196 | else 197 | local statelocation = smartfs._ldef.sfinv._make_state_location_(name) 198 | state = smartfs._makeState_(form, nil, statelocation, name) 199 | smartfs.inv[name] = state 200 | if form.form_setup_callback(state) ~= false then 201 | smartfs.inv[name] = state 202 | else 203 | return "" 204 | end 205 | end 206 | local fs = state:_buildFormspec_(false) 207 | return sfinv.make_formspec(player, context, fs, show_inv) 208 | end, 209 | on_player_receive_fields = function(self, player, _, fields) 210 | local name = player:get_player_name() 211 | if smartfs.inv[name] then 212 | smartfs.inv[name]:_sfs_on_receive_fields_(name, fields) 213 | end 214 | end, 215 | on_leave = function(self, player) 216 | local name = player:get_player_name() 217 | if smartfs.inv[name] then 218 | smartfs.inv[name].players:disconnect(name) 219 | smartfs.inv[name] = nil 220 | end 221 | end, 222 | }) 223 | end, 224 | _make_state_location_ = function(player) 225 | return { 226 | type = "inventory", 227 | inventory_handles_fields = true, 228 | player = player, 229 | _show_ = function(state) 230 | sfinv.set_player_inventory_formspec(minetest.get_player_by_name(state.location.player)) 231 | end, 232 | } 233 | end 234 | } 235 | 236 | -- Show to player 237 | smartfs._ldef.player = { 238 | _make_state_location_ = function(player) 239 | return { 240 | type = "player", 241 | player = player, 242 | _show_ = function(state) 243 | if not state._show_queued then 244 | state._show_queued = true 245 | minetest.after(0, function(state) 246 | if state then 247 | state._show_queued = nil 248 | if (not state.closed) and (not state.obsolete) then 249 | minetest.show_formspec(state.location.player, state.def.name, state:_buildFormspec_(true)) 250 | end 251 | end 252 | end, state) -- state given as reference. Maybe additional updates are done in the meantime or the form is obsolete 253 | end 254 | end 255 | } 256 | end 257 | } 258 | 259 | -- Standalone inventory 260 | smartfs._ldef.inventory = { 261 | set_inventory = function(form) 262 | if sfinv and sfinv.enabled then 263 | sfinv.enabled = nil 264 | end 265 | minetest.register_on_joinplayer(function(player) 266 | local name = player:get_player_name() 267 | local statelocation = smartfs._ldef.inventory._make_state_location_(name) 268 | local state = smartfs._makeState_(form, nil, statelocation, name) 269 | if form.form_setup_callback(state) ~= false then 270 | smartfs.inv[name] = state 271 | state:show() 272 | end 273 | end) 274 | minetest.register_on_leaveplayer(function(player) 275 | local name = player:get_player_name() 276 | smartfs.inv[name].obsolete = true 277 | smartfs.inv[name] = nil 278 | end) 279 | end, 280 | _make_state_location_ = function(name) 281 | return { 282 | type = "inventory", 283 | player = name, 284 | _show_ = function(state) 285 | if not state._show_queued then 286 | state._show_queued = true 287 | minetest.after(0, function(state) 288 | if state then 289 | state._show_queued = nil 290 | local player = minetest.get_player_by_name(state.location.player) 291 | --print("smartfs formspec:", state:_buildFormspec_(true)) 292 | player:set_inventory_formspec(state:_buildFormspec_(true)) 293 | end 294 | end, state) 295 | end 296 | end 297 | } 298 | end 299 | } 300 | 301 | -- Node metadata 302 | smartfs._ldef.nodemeta = { 303 | _make_state_location_ = function(nodepos) 304 | return { 305 | type = "nodemeta", 306 | pos = nodepos, 307 | _show_ = function(state) 308 | if not state._show_queued then 309 | state._show_queued = true 310 | minetest.after(0, function(state) 311 | if state then 312 | state._show_queued = nil 313 | local meta = minetest.get_meta(state.location.pos) 314 | meta:set_string("formspec", state:_buildFormspec_(true)) 315 | meta:set_string("smartfs_name", state.def.name) 316 | meta:mark_as_private("smartfs_name") 317 | end 318 | end, state) 319 | end 320 | end, 321 | } 322 | end 323 | } 324 | 325 | -- Sub-container (internally used) 326 | smartfs._ldef.container = { 327 | _make_state_location_ = function(element) 328 | local self = { 329 | type = "container", 330 | containerElement = element, 331 | parentState = element.root 332 | } 333 | if self.parentState.location.type == "container" then 334 | self.rootState = self.parentState.location.rootState 335 | else 336 | self.rootState = self.parentState 337 | end 338 | return self 339 | end 340 | } 341 | 342 | ------------------------------------------------------ 343 | -- Minetest Interface - on_receive_fields callback can be used in minetest.register_node for nodemeta forms 344 | ------------------------------------------------------ 345 | function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, params) 346 | local meta = minetest.get_meta(nodepos) 347 | local nodeform = meta:get_string("smartfs_name") 348 | if not nodeform then 349 | print("SmartFS - (Warning) smartfs.nodemeta_on_receive_fields for node without smarfs data") 350 | return false 351 | end 352 | 353 | -- get the currentsmartfs state 354 | local opened_id = minetest.pos_to_string(nodepos) 355 | local state 356 | local form = smartfs.get(nodeform) 357 | if not smartfs.opened[opened_id] or -- If opened first time 358 | smartfs.opened[opened_id].def.name ~= nodeform or -- Or form is changed 359 | smartfs.opened[opened_id].obsolete then 360 | local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) 361 | state = smartfs._makeState_(form, params, statelocation) 362 | if smartfs.opened[opened_id] then 363 | smartfs.opened[opened_id].obsolete = true 364 | end 365 | smartfs.opened[opened_id] = state 366 | form.form_setup_callback(state) 367 | else 368 | state = smartfs.opened[opened_id] 369 | end 370 | 371 | -- Set current sender check for multiple users on node 372 | local name 373 | if sender then 374 | name = sender:get_player_name() 375 | state.players:connect(name) 376 | end 377 | 378 | -- take the input 379 | state:_sfs_on_receive_fields_(name, fields) 380 | 381 | -- Reset form if all players disconnected 382 | if sender and not state.players:get_first() and not state.obsolete then 383 | local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) 384 | local resetstate = smartfs._makeState_(form, params, statelocation) 385 | if form.form_setup_callback(resetstate) ~= false then 386 | resetstate:show() 387 | end 388 | smartfs.opened[opened_id] = nil 389 | end 390 | end 391 | 392 | ------------------------------------------------------ 393 | -- Minetest Interface - on_player_receive_fields callback in case of inventory or player 394 | ------------------------------------------------------ 395 | minetest.register_on_player_receive_fields(function(player, formname, fields) 396 | local name = player:get_player_name() 397 | if smartfs.opened[name] and smartfs.opened[name].location.type == "player" then 398 | if smartfs.opened[name].def.name == formname then 399 | local state = smartfs.opened[name] 400 | state:_sfs_on_receive_fields_(name, fields) 401 | 402 | -- disconnect player if form closed 403 | if not state.players:get_first() then 404 | smartfs.opened[name].obsolete = true 405 | smartfs.opened[name] = nil 406 | end 407 | end 408 | elseif smartfs.inv[name] and smartfs.inv[name].location.type == "inventory" and not smartfs.inv[name].location.inventory_handles_fields then 409 | local state = smartfs.inv[name] 410 | state:_sfs_on_receive_fields_(name, fields) 411 | end 412 | return false 413 | end) 414 | 415 | ------------------------------------------------------ 416 | -- Minetest Interface - Notify loading of smartfs is done 417 | ------------------------------------------------------ 418 | minetest.after(0, function() 419 | smartfs.loaded = true 420 | end) 421 | 422 | ------------------------------------------------------ 423 | -- Form Interface [linked to form:show()] - Shows the form to a player 424 | ------------------------------------------------------ 425 | function smartfs._show_(form, name, params) 426 | assert(form) 427 | assert(type(name) == "string", "smartfs: name needs to be a string") 428 | assert(minetest.get_player_by_name(name), "player does not exist") 429 | local statelocation = smartfs._ldef.player._make_state_location_(name) 430 | local state = smartfs._makeState_(form, params, statelocation, name) 431 | if form.form_setup_callback(state) ~= false then 432 | if smartfs.opened[name] then -- set maybe previous form to obsolete 433 | smartfs.opened[name].obsolete = true 434 | end 435 | smartfs.opened[name] = state 436 | state:show() 437 | end 438 | return state 439 | end 440 | 441 | ------------------------------------------------------ 442 | -- Form Interface [linked to form:attach_to_node()] - Attach a formspec to a node meta 443 | ------------------------------------------------------ 444 | function smartfs._attach_to_node_(form, nodepos, params) 445 | assert(form) 446 | assert(nodepos and nodepos.x) 447 | 448 | local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) 449 | local state = smartfs._makeState_(form, params, statelocation) 450 | if form.form_setup_callback(state) ~= false then 451 | local opened_id = minetest.pos_to_string(nodepos) 452 | if smartfs.opened[opened_id] then -- set maybe previous form to obsolete 453 | smartfs.opened[opened_id].obsolete = true 454 | end 455 | state:show() 456 | end 457 | return state 458 | end 459 | 460 | ------------------------------------------------------ 461 | -- Smartfs Framework - Element class Methods 462 | ------------------------------------------------------ 463 | local element_class = {} 464 | local element_class_mt = { __index = element_class } 465 | 466 | function element_class:remove() 467 | self.root._ele[self.name] = nil 468 | end 469 | 470 | function element_class:setPosition(x,y) 471 | self.data.pos = {x=x,y=y} 472 | end 473 | 474 | function element_class:getPosition() 475 | return self.data.pos 476 | end 477 | 478 | function element_class:setSize(w,h) 479 | self.data.size = {w=w,h=h} 480 | end 481 | 482 | function element_class:getSize() 483 | return self.data.size 484 | end 485 | 486 | function element_class:setVisible(visible) 487 | if visible == nil then 488 | self.data.visible = true 489 | else 490 | self.data.visible = visible 491 | end 492 | end 493 | 494 | function element_class:getVisible() 495 | return self.data.visible 496 | end 497 | 498 | function element_class:getAbsName() 499 | return self.root:getNamespace()..self.name 500 | end 501 | 502 | function element_class:setBackground(image) 503 | self.data.background = image 504 | end 505 | 506 | function element_class:getBackground() 507 | return self.data.background 508 | end 509 | 510 | function element_class:getBackgroundString() 511 | if self.data.background then 512 | local size = self:getSize() 513 | if size then 514 | return "background[".. 515 | self.data.pos.x..","..self.data.pos.y..";".. 516 | size.w..","..size.h..";".. 517 | self.data.background.."]" 518 | else 519 | return "" 520 | end 521 | else 522 | return "" 523 | end 524 | end 525 | 526 | function element_class:setValue(value) 527 | self.data.value = value 528 | end 529 | 530 | function element_class:setTooltip(text) 531 | self.data.tooltip = minetest.formspec_escape(text) 532 | end 533 | 534 | function element_class:getTooltip() 535 | return self.data.tooltip 536 | end 537 | 538 | function element_class:getTooltipString() 539 | if self.data.tooltip then 540 | return "tooltip["..self:getAbsName()..";"..self:getTooltip().."]" 541 | else 542 | return "" 543 | end 544 | end 545 | 546 | ------------------------------------------------------ 547 | -- Smartfs Framework - State class Methods 548 | ------------------------------------------------------ 549 | local state_class = {} 550 | local state_class_mt = { __index = state_class } 551 | function state_class:get(name) 552 | return self._ele[name] 553 | end 554 | 555 | function state_class:close() 556 | self.closed = true 557 | end 558 | 559 | function state_class:getSize() 560 | return self._size 561 | end 562 | 563 | function state_class:size(w,h) 564 | self._size = {w=w,h=h} 565 | end 566 | state_class.setSize = state_class.size 567 | 568 | function state_class:getNamespace() 569 | local ref = self 570 | local namespace = "" 571 | while ref.location.type == "container" do 572 | namespace = ref.location.containerElement.name.."#"..namespace 573 | ref = ref.location.parentState -- step near to the root 574 | end 575 | return namespace 576 | end 577 | 578 | function state_class:_buildFormspec_(size) 579 | local res = "" 580 | if self._size and size then 581 | res = "size["..self._size.w..","..self._size.h.."]" 582 | end 583 | for key,val in pairs(self._ele) do 584 | if val:getVisible() then 585 | res = res .. val:getBackgroundString() .. val:build() .. val:getTooltipString() 586 | end 587 | end 588 | return res 589 | end 590 | 591 | function state_class:_get_element_recursive_(field) 592 | local topfield 593 | for z in field:gmatch("[^#]+") do 594 | topfield = z 595 | break 596 | end 597 | local element = self._ele[topfield] 598 | if element and field == topfield then 599 | return element 600 | elseif element then 601 | if element._getSubElement_ then 602 | local rel_field = string.sub(field, string.len(topfield)+2) 603 | return element:_getSubElement_(rel_field) 604 | else 605 | return element 606 | end 607 | else 608 | return nil 609 | end 610 | end 611 | 612 | -- process onInput hook for the state 613 | function state_class:_sfs_process_oninput_(fields, player) 614 | if self._onInput then 615 | self:_onInput(fields, player) 616 | end 617 | -- recursive all onInput hooks on visible containers 618 | for elename, eledef in pairs(self._ele) do 619 | if eledef.getContainerState and eledef:getVisible() then 620 | eledef:getContainerState():_sfs_process_oninput_(fields, player) 621 | end 622 | end 623 | end 624 | 625 | -- Receive fields and actions from formspec 626 | function state_class:_sfs_on_receive_fields_(player, fields) 627 | local fields_todo = {} 628 | for field, value in pairs(fields) do 629 | local element = self:_get_element_recursive_(field) 630 | if element then 631 | fields_todo[field] = { element = element, value = value } 632 | end 633 | end 634 | 635 | for field, todo in pairs(fields_todo) do 636 | todo.element:setValue(todo.value) 637 | end 638 | 639 | self:_sfs_process_oninput_(fields, player) 640 | 641 | for field, todo in pairs(fields_todo) do 642 | if todo.element.submit then 643 | todo.element:submit(todo.value, player) 644 | end 645 | end 646 | -- handle key_enter 647 | if fields.key_enter and fields.key_enter_field then 648 | local element = self:_get_element_recursive_(fields.key_enter_field) 649 | if element and element.submit_key_enter then 650 | element:submit_key_enter(fields[fields.key_enter_field], player) 651 | end 652 | end 653 | 654 | if not fields.quit and not self.closed and not self.obsolete then 655 | self:show() 656 | else 657 | self.players:disconnect(player) 658 | if not fields.quit and self.closed and not self.obsolete then 659 | --closed by application (without fields.quit). currently not supported, see: https://github.com/minetest/minetest/pull/4675 660 | minetest.show_formspec(player,"","size[5,1]label[0,0;Formspec closing not yet created!]") 661 | end 662 | end 663 | return true 664 | end 665 | 666 | function state_class:onInput(func) 667 | self._onInput = func -- (fields, player) 668 | end 669 | 670 | function state_class:load(file) 671 | local file = io.open(file, "r") 672 | if file then 673 | local table = minetest.deserialize(file:read("*all")) 674 | if type(table) == "table" then 675 | if table.size then 676 | self._size = table.size 677 | end 678 | for key,val in pairs(table.ele) do 679 | self:element(val.type,val) 680 | end 681 | return true 682 | end 683 | end 684 | return false 685 | end 686 | 687 | function state_class:save(file) 688 | local res = {ele={}} 689 | 690 | if self._size then 691 | res.size = self._size 692 | end 693 | 694 | for key,val in pairs(self._ele) do 695 | res.ele[key] = val.data 696 | end 697 | 698 | local file = io.open(file, "w") 699 | if file then 700 | file:write(minetest.serialize(res)) 701 | file:close() 702 | return true 703 | end 704 | return false 705 | end 706 | 707 | function state_class:setparam(key,value) 708 | if not key then return end 709 | self.param[key] = value 710 | return true 711 | end 712 | 713 | function state_class:getparam(key,default) 714 | if not key then return end 715 | return self.param[key] or default 716 | end 717 | 718 | function state_class:element(typen,data) 719 | local type = smartfs._edef[typen] 720 | assert(type, "Element type "..typen.." does not exist!") 721 | assert(not self._ele[data.name], "Element "..data.name.." already exists") 722 | 723 | local ele = setmetatable({}, element_class_mt) 724 | ele.name = data.name 725 | ele.root = self 726 | ele.data = data 727 | ele.data.type = typen 728 | ele.data.visible = true 729 | for key, val in pairs(type) do 730 | ele[key] = val 731 | end 732 | self._ele[data.name] = ele 733 | type.onCreate(ele) 734 | return self._ele[data.name] 735 | end 736 | 737 | ------------------------------------------------------ 738 | -- Smartfs Framework - State class Methods - Build time only 739 | ------------------------------------------------------ 740 | function state_class:button(x, y, w, h, name, text, exitf) 741 | return self:element("button", { 742 | pos = {x=x,y=y}, 743 | size = {w=w,h=h}, 744 | name = name, 745 | value = text, 746 | closes = exitf or false 747 | }) 748 | end 749 | 750 | function state_class:image_button(x, y, w, h, name, text, image, exitf) 751 | return self:element("button", { 752 | pos = {x=x,y=y}, 753 | size = {w=w,h=h}, 754 | name = name, 755 | value = text, 756 | image = image, 757 | closes = exitf or false 758 | }) 759 | end 760 | 761 | function state_class:item_image_button(x, y, w, h, name, text, item, exitf) 762 | return self:element("button", { 763 | pos = {x=x,y=y}, 764 | size = {w=w,h=h}, 765 | name = name, 766 | value = text, 767 | item = item, 768 | closes = exitf or false 769 | }) 770 | end 771 | 772 | function state_class:label(x, y, name, text) 773 | return self:element("label", { 774 | pos = {x=x,y=y}, 775 | name = name, 776 | value = text, 777 | vertical = false 778 | }) 779 | end 780 | 781 | function state_class:vertlabel(x, y, name, text) 782 | return self:element("label", { 783 | pos = {x=x,y=y}, 784 | name = name, 785 | value = text, 786 | vertical = true 787 | }) 788 | end 789 | 790 | function state_class:toggle(x, y, w, h, name, list) 791 | return self:element("toggle", { 792 | pos = {x=x, y=y}, 793 | size = {w=w, h=h}, 794 | name = name, 795 | id = 1, 796 | list = list 797 | }) 798 | end 799 | 800 | function state_class:field(x, y, w, h, name, label) 801 | return self:element("field", { 802 | pos = {x=x, y=y}, 803 | size = {w=w, h=h}, 804 | name = name, 805 | value = "", 806 | label = label 807 | }) 808 | end 809 | 810 | function state_class:pwdfield(x, y, w, h, name, label) 811 | local res = self:element("field", { 812 | pos = {x=x, y=y}, 813 | size = {w=w, h=h}, 814 | name = name, 815 | value = "", 816 | label = label 817 | }) 818 | res:isPassword(true) 819 | return res 820 | end 821 | 822 | function state_class:textarea(x, y, w, h, name, label) 823 | local res = self:element("field", { 824 | pos = {x=x, y=y}, 825 | size = {w=w, h=h}, 826 | name = name, 827 | value = "", 828 | label = label 829 | }) 830 | res:isMultiline(true) 831 | return res 832 | end 833 | 834 | function state_class:image(x, y, w, h, name, img) 835 | return self:element("image", { 836 | pos = {x=x, y=y}, 837 | size = {w=w, h=h}, 838 | name = name, 839 | value = img, 840 | imgtype = "image" 841 | }) 842 | end 843 | 844 | function state_class:background(x, y, w, h, name, img) 845 | return self:element("image", { 846 | pos = {x=x, y=y}, 847 | size = {w=w, h=h}, 848 | name = name, 849 | background = img, 850 | imgtype = "background" 851 | }) 852 | end 853 | 854 | function state_class:item_image(x, y, w, h, name, img) 855 | return self:element("image", { 856 | pos = {x=x, y=y}, 857 | size = {w=w, h=h}, 858 | name = name, 859 | value = img, 860 | imgtype = "item" 861 | }) 862 | end 863 | 864 | function state_class:checkbox(x, y, name, label, selected) 865 | return self:element("checkbox", { 866 | pos = {x=x, y=y}, 867 | name = name, 868 | value = selected, 869 | label = label 870 | }) 871 | end 872 | 873 | function state_class:listbox(x, y, w, h, name, selected, transparent) 874 | return self:element("list", { 875 | pos = {x=x, y=y}, 876 | size = {w=w, h=h}, 877 | name = name, 878 | selected = selected, 879 | transparent = transparent 880 | }) 881 | end 882 | 883 | function state_class:dropdown(x, y, w, h, name, selected) 884 | return self:element("dropdown", { 885 | pos = {x=x, y=y}, 886 | size = {w=w, h=h}, 887 | name = name, 888 | selected = selected 889 | }) 890 | end 891 | 892 | function state_class:inventory(x, y, w, h, name) 893 | return self:element("inventory", { 894 | pos = {x=x, y=y}, 895 | size = {w=w, h=h}, 896 | name = name 897 | }) 898 | end 899 | 900 | function state_class:container(x, y, name, relative) 901 | return self:element("container", { 902 | pos = {x=x, y=y}, 903 | name = name, 904 | relative = false 905 | }) 906 | end 907 | 908 | function state_class:view(x, y, name, relative) 909 | return self:element("container", { 910 | pos = {x=x, y=y}, 911 | name = name, 912 | relative = true 913 | }) 914 | end 915 | 916 | ------------------------------------------------------ 917 | -- Smartfs Framework - create a form object (state) 918 | ------------------------------------------------------ 919 | function smartfs._makeState_(form, params, location, newplayer) 920 | ------------------------------------------------------ 921 | -- State - -- Object to manage players 922 | ------------------------------------------------------ 923 | local function _make_players_(newplayer) 924 | local self = { 925 | _list = {} 926 | } 927 | function self.connect(self, player) 928 | self._list[player] = true 929 | end 930 | function self.disconnect(self, player) 931 | self._list[player] = nil 932 | end 933 | function self.get_first(self) 934 | return next(self._list) 935 | end 936 | if newplayer then 937 | self:connect(newplayer) 938 | end 939 | return self 940 | end 941 | 942 | local state = { 943 | _ele = {}, 944 | def = form, 945 | players = _make_players_(newplayer), 946 | location = location, 947 | is_inv = (location.type == "inventory"), -- obsolete. Please use location.type="inventory" instead 948 | player = newplayer, -- obsolete. Please use location.player 949 | param = params or {}, 950 | show = location._show_, 951 | } 952 | return setmetatable(state, state_class_mt) 953 | end 954 | 955 | ----------------------------------------------------------------- 956 | ------------------------- ELEMENTS ---------------------------- 957 | ----------------------------------------------------------------- 958 | 959 | smartfs.element("button", { 960 | onCreate = function(self) 961 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "button needs valid pos") 962 | assert(self.data.size and self.data.size.w and self.data.size.h, "button needs valid size") 963 | assert(self.name, "button needs name") 964 | assert(self.data.value, "button needs label") 965 | end, 966 | build = function(self) 967 | local specstring 968 | if self.data.image then 969 | if self.data.closes then 970 | specstring = "image_button_exit[" 971 | else 972 | specstring = "image_button[" 973 | end 974 | elseif self.data.item then 975 | if self.data.closes then 976 | specstring = "item_image_button_exit[" 977 | else 978 | specstring = "item_image_button[" 979 | end 980 | else 981 | if self.data.closes then 982 | specstring = "button_exit[" 983 | else 984 | specstring = "button[" 985 | end 986 | end 987 | 988 | specstring = specstring .. 989 | self.data.pos.x..","..self.data.pos.y..";".. 990 | self.data.size.w..","..self.data.size.h..";" 991 | if self.data.image then 992 | specstring = specstring..minetest.formspec_escape(self.data.image)..";" 993 | elseif self.data.item then 994 | specstring = specstring..self.data.item..";" 995 | end 996 | specstring = specstring..self:getAbsName()..";".. 997 | minetest.formspec_escape(self.data.value).."]" 998 | return specstring 999 | end, 1000 | submit = function(self, field, player) 1001 | if self._click then 1002 | self:_click(self.root, player) 1003 | end 1004 | end, 1005 | onClick = function(self,func) 1006 | self._click = func 1007 | end, 1008 | click = function(self,func) 1009 | self._click = func 1010 | end, 1011 | setText = function(self,text) 1012 | self:setValue(text) 1013 | end, 1014 | getText = function(self) 1015 | return self.data.value 1016 | end, 1017 | setImage = function(self,image) 1018 | self.data.image = image 1019 | self.data.item = nil 1020 | end, 1021 | getImage = function(self) 1022 | return self.data.image 1023 | end, 1024 | setItem = function(self,item) 1025 | self.data.item = item 1026 | self.data.image = nil 1027 | end, 1028 | getItem = function(self) 1029 | return self.data.item 1030 | end, 1031 | setClose = function(self,bool) 1032 | self.data.closes = bool 1033 | end, 1034 | getClose = function(self) 1035 | return self.data.closes or false 1036 | end 1037 | }) 1038 | 1039 | smartfs.element("toggle", { 1040 | onCreate = function(self) 1041 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "toggle needs valid pos") 1042 | assert(self.data.size and self.data.size.w and self.data.size.h, "toggle needs valid size") 1043 | assert(self.name, "toggle needs name") 1044 | assert(self.data.list, "toggle needs data") 1045 | end, 1046 | build = function(self) 1047 | return "button[".. 1048 | self.data.pos.x..","..self.data.pos.y.. 1049 | ";".. 1050 | self.data.size.w..","..self.data.size.h.. 1051 | ";".. 1052 | self:getAbsName().. 1053 | ";".. 1054 | minetest.formspec_escape(self.data.list[self.data.id]).. 1055 | "]" 1056 | end, 1057 | submit = function(self, field, player) 1058 | self.data.id = self.data.id + 1 1059 | if self.data.id > #self.data.list then 1060 | self.data.id = 1 1061 | end 1062 | if self._tog then 1063 | self:_tog(self.root, player) 1064 | end 1065 | end, 1066 | onToggle = function(self,func) 1067 | self._tog = func 1068 | end, 1069 | setId = function(self,id) 1070 | self.data.id = id 1071 | end, 1072 | getId = function(self) 1073 | return self.data.id 1074 | end, 1075 | getText = function(self) 1076 | return self.data.list[self.data.id] 1077 | end 1078 | }) 1079 | 1080 | smartfs.element("label", { 1081 | onCreate = function(self) 1082 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "label needs valid pos") 1083 | assert(self.data.value, "label needs text") 1084 | end, 1085 | build = function(self) 1086 | local specstring 1087 | if self.data.vertical then 1088 | specstring = "vertlabel[" 1089 | else 1090 | specstring = "label[" 1091 | end 1092 | return specstring.. 1093 | self.data.pos.x..","..self.data.pos.y.. 1094 | ";".. 1095 | minetest.formspec_escape(self.data.value).. 1096 | "]" 1097 | end, 1098 | setText = function(self,text) 1099 | self:setValue(text) 1100 | end, 1101 | getText = function(self) 1102 | return self.data.value 1103 | end 1104 | }) 1105 | 1106 | smartfs.element("field", { 1107 | onCreate = function(self) 1108 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "field needs valid pos") 1109 | assert(self.data.size and self.data.size.w and self.data.size.h, "field needs valid size") 1110 | assert(self.name, "field needs name") 1111 | self.data.value = self.data.value or "" 1112 | self.data.label = self.data.label or "" 1113 | end, 1114 | build = function(self) 1115 | if self.data.ml then 1116 | return "textarea[".. 1117 | self.data.pos.x..","..self.data.pos.y.. 1118 | ";".. 1119 | self.data.size.w..","..self.data.size.h.. 1120 | ";".. 1121 | self:getAbsName().. 1122 | ";".. 1123 | minetest.formspec_escape(self.data.label).. 1124 | ";".. 1125 | minetest.formspec_escape(self.data.value).. 1126 | "]" 1127 | elseif self.data.pwd then 1128 | return "pwdfield[".. 1129 | self.data.pos.x..","..self.data.pos.y.. 1130 | ";".. 1131 | self.data.size.w..","..self.data.size.h.. 1132 | ";".. 1133 | self:getAbsName().. 1134 | ";".. 1135 | minetest.formspec_escape(self.data.label).. 1136 | "]".. 1137 | self:getCloseOnEnterString() 1138 | else 1139 | return "field[".. 1140 | self.data.pos.x..","..self.data.pos.y.. 1141 | ";".. 1142 | self.data.size.w..","..self.data.size.h.. 1143 | ";".. 1144 | self:getAbsName().. 1145 | ";".. 1146 | minetest.formspec_escape(self.data.label).. 1147 | ";".. 1148 | minetest.formspec_escape(self.data.value).. 1149 | "]".. 1150 | self:getCloseOnEnterString() 1151 | end 1152 | end, 1153 | setText = function(self,text) 1154 | self:setValue(text) 1155 | end, 1156 | getText = function(self) 1157 | return self.data.value 1158 | end, 1159 | isPassword = function(self,bool) 1160 | self.data.pwd = bool 1161 | end, 1162 | isMultiline = function(self,bool) 1163 | self.data.ml = bool 1164 | end, 1165 | getCloseOnEnterString = function(self) 1166 | if self.close_on_enter == nil then 1167 | return "" 1168 | else 1169 | return "field_close_on_enter["..self:getAbsName()..";"..tostring(self.close_on_enter).."]" 1170 | end 1171 | end, 1172 | setCloseOnEnter = function(self, value) 1173 | self.close_on_enter = value 1174 | end, 1175 | getCloseOnEnter = function(self) 1176 | return self.close_on_enter 1177 | end, 1178 | submit_key_enter = function(self, field, player) 1179 | if self._key_enter then 1180 | self:_key_enter(self.root, player) 1181 | end 1182 | end, 1183 | onKeyEnter = function(self,func) 1184 | self._key_enter = func 1185 | end, 1186 | }) 1187 | 1188 | smartfs.element("image", { 1189 | onCreate = function(self) 1190 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "image needs valid pos") 1191 | assert(self.data.size and self.data.size.w and self.data.size.h, "image needs valid size") 1192 | self.data.value = self.data.value or "" 1193 | end, 1194 | build = function(self) 1195 | if self.data.imgtype == "background" then 1196 | return "" -- handled in _buildFormspec_ trough getBackgroundString() 1197 | elseif self.data.imgtype == "item" then 1198 | return "item_image[".. 1199 | self.data.pos.x..","..self.data.pos.y.. 1200 | ";".. 1201 | self.data.size.w..","..self.data.size.h.. 1202 | ";".. 1203 | minetest.formspec_escape(self.data.value).. 1204 | "]" 1205 | else 1206 | return "image[".. 1207 | self.data.pos.x..","..self.data.pos.y.. 1208 | ";".. 1209 | self.data.size.w..","..self.data.size.h.. 1210 | ";".. 1211 | minetest.formspec_escape(self.data.value).. 1212 | "]" 1213 | end 1214 | end, 1215 | setImage = function(self,text) 1216 | if self.data.imgtype == "background" then 1217 | self.data.background = text 1218 | else 1219 | self:setValue(text) 1220 | end 1221 | end, 1222 | getImage = function(self) 1223 | if self.data.imgtype == "background" then 1224 | return self.data.background 1225 | else 1226 | return self.data.value 1227 | end 1228 | end 1229 | }) 1230 | 1231 | smartfs.element("checkbox", { 1232 | onCreate = function(self) 1233 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "checkbox needs valid pos") 1234 | assert(self.name, "checkbox needs name") 1235 | self.data.value = minetest.is_yes(self.data.value) 1236 | self.data.label = self.data.label or "" 1237 | end, 1238 | build = function(self) 1239 | return "checkbox[".. 1240 | self.data.pos.x..","..self.data.pos.y.. 1241 | ";".. 1242 | self:getAbsName().. 1243 | ";".. 1244 | minetest.formspec_escape(self.data.label).. 1245 | ";" .. boolToStr(self.data.value) .."]" 1246 | end, 1247 | submit = function(self, field, player) 1248 | -- call the toggle function if defined 1249 | if self._tog then 1250 | self:_tog(self.root, player) 1251 | end 1252 | end, 1253 | setValue = function(self, value) 1254 | self.data.value = minetest.is_yes(value) 1255 | end, 1256 | getValue = function(self) 1257 | return self.data.value 1258 | end, 1259 | onToggle = function(self,func) 1260 | self._tog = func 1261 | end, 1262 | }) 1263 | 1264 | smartfs.element("list", { 1265 | onCreate = function(self) 1266 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") 1267 | assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") 1268 | assert(self.name, "list needs name") 1269 | self.data.value = minetest.is_yes(self.data.value) 1270 | self.data.items = self.data.items or {} 1271 | end, 1272 | build = function(self) 1273 | if not self.data.items then 1274 | self.data.items = {} 1275 | end 1276 | return "textlist[".. 1277 | self.data.pos.x..","..self.data.pos.y.. 1278 | ";".. 1279 | self.data.size.w..","..self.data.size.h.. 1280 | ";".. 1281 | self:getAbsName().. 1282 | ";".. 1283 | table.concat(self.data.items, ",").. 1284 | ";".. 1285 | tostring(self.data.selected or "").. 1286 | ";".. 1287 | tostring(self.data.transparent or "false").."]" 1288 | end, 1289 | submit = function(self, field, player) 1290 | local _type = string.sub(field,1,3) 1291 | local index = tonumber(string.sub(field,5)) 1292 | self.data.selected = index 1293 | if _type == "CHG" and self._click then 1294 | self:_click(self.root, index, player) 1295 | elseif _type == "DCL" and self._doubleClick then 1296 | self:_doubleClick(self.root, index, player) 1297 | end 1298 | end, 1299 | onClick = function(self, func) 1300 | self._click = func 1301 | end, 1302 | click = function(self, func) 1303 | self._click = func 1304 | end, 1305 | onDoubleClick = function(self, func) 1306 | self._doubleClick = func 1307 | end, 1308 | doubleclick = function(self, func) 1309 | self._doubleClick = func 1310 | end, 1311 | addItem = function(self, item) 1312 | table.insert(self.data.items, minetest.formspec_escape(item)) 1313 | -- return the index of item. It is the last one 1314 | return #self.data.items 1315 | end, 1316 | removeItem = function(self,idx) 1317 | table.remove(self.data.items,idx) 1318 | end, 1319 | getItem = function(self, idx) 1320 | return self.data.items[idx] 1321 | end, 1322 | popItem = function(self) 1323 | local item = self.data.items[#self.data.items] 1324 | table.remove(self.data.items) 1325 | return item 1326 | end, 1327 | clearItems = function(self) 1328 | self.data.items = {} 1329 | end, 1330 | setSelected = function(self,idx) 1331 | self.data.selected = idx 1332 | end, 1333 | getSelected = function(self) 1334 | return self.data.selected 1335 | end, 1336 | getSelectedItem = function(self) 1337 | return self:getItem(self:getSelected()) 1338 | end, 1339 | }) 1340 | 1341 | smartfs.element("dropdown", { 1342 | onCreate = function(self) 1343 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "dropdown needs valid pos") 1344 | assert(self.data.size and self.data.size.w and self.data.size.h, "dropdown needs valid size") 1345 | assert(self.name, "dropdown needs name") 1346 | self.data.items = self.data.items or {} 1347 | self.data.selected = self.data.selected or 1 1348 | self.data.value = "" 1349 | end, 1350 | build = function(self) 1351 | return "dropdown[".. 1352 | self.data.pos.x..","..self.data.pos.y.. 1353 | ";".. 1354 | self.data.size.w..","..self.data.size.h.. 1355 | ";".. 1356 | self:getAbsName().. 1357 | ";".. 1358 | table.concat(self.data.items, ",").. 1359 | ";".. 1360 | tostring(self:getSelected()).. 1361 | "]" 1362 | end, 1363 | submit = function(self, field, player) 1364 | self:getSelected() 1365 | if self._select then 1366 | self:_select(self.root, field, player) 1367 | end 1368 | end, 1369 | onSelect = function(self, func) 1370 | self._select = func 1371 | end, 1372 | addItem = function(self, item) 1373 | table.insert(self.data.items, item) 1374 | if #self.data.items == self.data.selected then 1375 | self.data.value = item 1376 | end 1377 | -- return the index of item. It is the last one 1378 | return #self.data.items 1379 | end, 1380 | removeItem = function(self,idx) 1381 | table.remove(self.data.items,idx) 1382 | end, 1383 | getItem = function(self, idx) 1384 | return self.data.items[idx] 1385 | end, 1386 | popItem = function(self) 1387 | local item = self.data.items[#self.data.items] 1388 | table.remove(self.data.items) 1389 | return item 1390 | end, 1391 | clearItems = function(self) 1392 | self.data.items = {} 1393 | end, 1394 | setSelected = function(self,idx) 1395 | self.data.selected = idx 1396 | self.data.value = self:getItem(idx) or "" 1397 | end, 1398 | getSelected = function(self) 1399 | self.data.selected = 1 1400 | if #self.data.items > 1 then 1401 | for i = 1, #self.data.items do 1402 | if self.data.items[i] == self.data.value then 1403 | self.data.selected = i 1404 | end 1405 | end 1406 | end 1407 | return self.data.selected 1408 | end, 1409 | getSelectedItem = function(self) 1410 | return self.data.value 1411 | end, 1412 | }) 1413 | 1414 | smartfs.element("inventory", { 1415 | onCreate = function(self) 1416 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") 1417 | assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") 1418 | assert(self.name, "list needs name") 1419 | 1420 | -- Default the list name to the element name. 1421 | self.data.list = self.name 1422 | end, 1423 | build = function(self) 1424 | return "list[".. 1425 | (self.data.invlocation or "current_player") .. 1426 | ";".. 1427 | self.data.list .. 1428 | ";".. 1429 | self.data.pos.x..","..self.data.pos.y.. 1430 | ";".. 1431 | self.data.size.w..","..self.data.size.h.. 1432 | ";".. 1433 | (self.data.index or "") .. 1434 | "]" 1435 | end, 1436 | -- available inventory locations 1437 | -- "current_player": Player to whom the menu is shown 1438 | -- "player:": Any player 1439 | -- "nodemeta:,,": Any node metadata 1440 | -- "detached:": A detached inventory 1441 | -- "context" does not apply to smartfs, since there is no node-metadata as context available 1442 | setLocation = function(self,invlocation) 1443 | self.data.invlocation = invlocation 1444 | end, 1445 | getLocation = function(self) 1446 | return self.data.invlocation or "current_player" 1447 | end, 1448 | usePosition = function(self, pos) 1449 | self.data.invlocation = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z) 1450 | end, 1451 | usePlayer = function(self, name) 1452 | self.data.invlocation = "player:" .. name 1453 | end, 1454 | useDetached = function(self, name) 1455 | self.data.invlocation = "detached:" .. name 1456 | end, 1457 | setList = function(self, list) 1458 | self.data.list = list or self.name 1459 | end, 1460 | getList = function(self) 1461 | return self.data.list 1462 | end, 1463 | setIndex = function(self,index) 1464 | self.data.index = index 1465 | end, 1466 | getIndex = function(self) 1467 | return self.data.index 1468 | end 1469 | }) 1470 | 1471 | smartfs.element("code", { 1472 | onCreate = function(self) 1473 | self.data.code = self.data.code or "" 1474 | end, 1475 | build = function(self) 1476 | if self._build then 1477 | self:_build() 1478 | end 1479 | 1480 | return self.data.code 1481 | end, 1482 | submit = function(self, field, player) 1483 | if self._sub then 1484 | self:_sub(self.root, field, player) 1485 | end 1486 | end, 1487 | onSubmit = function(self,func) 1488 | self._sub = func 1489 | end, 1490 | onBuild = function(self,func) 1491 | self._build = func 1492 | end, 1493 | setCode = function(self,code) 1494 | self.data.code = code 1495 | end, 1496 | getCode = function(self) 1497 | return self.data.code 1498 | end 1499 | }) 1500 | 1501 | smartfs.element("container", { 1502 | onCreate = function(self) 1503 | assert(self.data.pos and self.data.pos.x and self.data.pos.y, "container needs valid pos") 1504 | assert(self.name, "container needs name") 1505 | local statelocation = smartfs._ldef.container._make_state_location_(self) 1506 | self._state = smartfs._makeState_(nil, self.root.param, statelocation) 1507 | end, 1508 | 1509 | -- redefinitions. The size is not handled by data.size but by container-state:size 1510 | setSize = function(self,w,h) 1511 | self:getContainerState():setSize(w,h) 1512 | end, 1513 | getSize = function(self) 1514 | return self:getContainerState():getSize() 1515 | end, 1516 | 1517 | -- element interface methods 1518 | build = function(self) 1519 | if self.data.relative ~= true then 1520 | return "container["..self.data.pos.x..","..self.data.pos.y.."]".. 1521 | self:getContainerState():_buildFormspec_(false).. 1522 | "container_end[]" 1523 | else 1524 | return self:getContainerState():_buildFormspec_(false) 1525 | end 1526 | end, 1527 | getContainerState = function(self) 1528 | return self._state 1529 | end, 1530 | _getSubElement_ = function(self, field) 1531 | return self:getContainerState():_get_element_recursive_(field) 1532 | end, 1533 | }) 1534 | 1535 | return smartfs 1536 | --------------------------------------------------------------------------------