z_mirror/RateCalculator
git clone https://git.raiguard.me/z_mirror/RateCalculator
75c18ca
trunk
1local flib_array =require ( "__flib__.array" ) 2local flib_gui =require ( "__flib__.gui" ) 3local flib_position =require ( "__flib__.position" ) 4local flib_table =require ( "__flib__.table" ) 5 6local gui_rates =require ( "scripts.gui-rates" ) 7local gui_util =require ( "scripts.gui-util" ) 8 9--- @class GuiData 10--- @field elems table<string, LuaGuiElement> 11--- @field inserter_divisor EntityWithQualityID 12--- @field manual_multiplier double 13--- @field materials_divisor string? 14--- @field pinned boolean 15--- @field player LuaPlayer 16--- @field search_open boolean 17--- @field search_query string 18--- @field selected_set_index integer 19--- @field selected_timescale Timescale 20--- @field sets CalculationSet[] 21--- @field transport_belt_divisor EntityWithQualityID 22--- @field display_data_lookup DisplayDataLookup 23 24--- @type GuiLocation 25local top_left_location ={ x =15 , y =58 + 15 } 26 27--- @param self GuiData 28local function reset_location ( self ) 29local value =self . player . mod_settings [ "rcalc-default-gui-location" ]. value 30local window =self . elems . rcalc_window 31if value == "top-left" then 32local scale =self . player . display_scale 33window . location =flib_position . mul ( top_left_location , { scale , scale } ) 34else 35window . auto_center =true 36end 37end 38 39--- @param self GuiData 40local function update_gui ( self ) 41local sets =self . sets 42local selected_set_index =self . selected_set_index 43local set =sets [ selected_set_index ] 44if not set then 45return 46end 47 48local elems =self . elems 49 50local nav_backward_button =elems . nav_backward_button 51local at_back =selected_set_index == 1 52nav_backward_button . enabled =not at_back 53nav_backward_button . tooltip ={ "gui.rcalc-previous-set" , selected_set_index , # sets } 54 55local nav_forward_button =elems . nav_forward_button 56local at_front =selected_set_index == # sets 57nav_forward_button . enabled =not at_front 58nav_forward_button . tooltip ={ "gui.rcalc-next-set" , selected_set_index , # sets } 59 60local timescale =self . selected_timescale 61local timescale_data =gui_util . timescale_data [ timescale ] 62 63local timescale_divisor_chooser =elems . timescale_divisor_chooser 64local divisor_source =timescale_data . divisor_source 65if divisor_source then 66timescale_divisor_chooser . visible =true 67timescale_divisor_chooser . elem_filters =storage . elem_filters [ divisor_source ] 68timescale_divisor_chooser . elem_value =self [ divisor_source ] 69else 70timescale_divisor_chooser . visible =false 71end 72elems . timescale_dropdown . selected_index =flib_array . find ( gui_util . ordered_timescales , timescale ) --[[@as uint]] 73elems . multiplier_textfield . text =tostring ( self . manual_multiplier ) 74 75set . errors [ "inserter-rates-estimates" ] =divisor_source == "inserter_divisor" and true or nil 76 77local show_checkboxes =self . player . mod_settings [ "rcalc-show-completion-checkboxes" ]. value --[[@as boolean]] 78local show_intermediate_breakdowns =self . player . mod_settings [ "rcalc-show-intermediate-breakdowns" ]. value --[[@as boolean]] 79self . elems . rates_scroll_pane . style . minimal_width =500 80+ ( show_checkboxes and 44 or 0 ) 81+ ( show_intermediate_breakdowns and 50 or 0 ) 82 83local category_display_data =gui_rates . update_display_data ( self , set ) 84gui_rates . update_gui ( self , category_display_data ) 85 86local errors_frame =self . elems . errors_frame 87errors_frame . clear () 88local visible =false 89if self . player . mod_settings [ "rcalc-show-calculation-errors" ]. value then 90for error in pairs ( set . errors ) do 91visible =true 92errors_frame . add ( { 93type ="label" , 94style ="bold_label" , 95caption ={ "" , "[img=warning-white] " , { "gui.rcalc-error-" .. error } } , 96tooltip ={ "?" , { "gui.rcalc-error-" .. error .. "-description" } , "" } , 97} ) 98end 99end 100errors_frame . visible =visible 101end 102 103--- @param self GuiData 104local function toggle_search ( self ) 105local search_open =not self . search_open 106self . search_open =search_open 107local button =self . elems . search_button 108button . toggled =self . search_open 109local textfield =self . elems . search_textfield 110textfield . visible =search_open 111self . search_open =search_open 112if search_open then 113textfield . focus () 114textfield . select_all () 115else 116textfield . text ="" 117self . search_query ="" 118update_gui ( self ) 119end 120end 121 122--- @param e EventData.on_gui_click 123local function on_window_closed ( e ) 124local self =storage . gui [ e . player_index ] 125if not self or self . pinned then 126return 127end 128if self . search_open then 129toggle_search ( self ) 130self . player . opened =self . elems . rcalc_window 131return 132end 133self . elems . timescale_dropdown . close_dropdown () 134self . elems . rcalc_window . visible =false 135end 136 137--- @param e EventData.on_gui_click 138local function on_titlebar_click ( e ) 139local self =storage . gui [ e . player_index ] 140if not self or e . button ~= defines . mouse_button_type . middle then 141return 142end 143reset_location ( self ) 144end 145 146--- @param e EventData.on_gui_click 147local function on_close_button_click ( e ) 148local self =storage . gui [ e . player_index ] 149if not self then 150return 151end 152self . elems . rcalc_window . visible =false 153if self . player . opened == self . elems . rcalc_window then 154self . player . opened =nil 155end 156end 157 158--- @param e EventData.on_gui_click 159local function on_pin_button_click ( e ) 160local self =storage . gui [ e . player_index ] 161if not self then 162return 163end 164local pinned =not self . pinned 165self . pinned =pinned 166e . element . toggled =pinned 167if pinned then 168self . player . opened =nil 169self . elems . close_button . tooltip ={ "gui.close" } 170self . elems . search_button . tooltip ={ "gui.search" } 171else 172self . player . opened =self . elems . rcalc_window 173self . elems . close_button . tooltip ={ "gui.close-instruction" } 174self . elems . search_button . tooltip ={ "gui.flib-search-instruction" } 175end 176end 177 178--- @param e EventData.on_gui_click 179local function on_nav_backward_button_click ( e ) 180local self =storage . gui [ e . player_index ] 181if not self then 182return 183end 184self . selected_set_index =math . max ( self . selected_set_index - 1 , 1 ) 185update_gui ( self ) 186end 187 188--- @param e EventData.on_gui_click 189local function on_nav_forward_button_click ( e ) 190local self =storage . gui [ e . player_index ] 191if not self then 192return 193end 194self . selected_set_index =math . min ( self . selected_set_index + 1 , # self . sets ) 195update_gui ( self ) 196end 197 198--- @param e EventData.on_gui_click 199local function on_search_button_click ( e ) 200local self =storage . gui [ e . player_index ] 201if not self then 202return 203end 204toggle_search ( self ) 205end 206 207--- @param e EventData.on_gui_text_changed 208local function on_search_text_changed ( e ) 209local self =storage . gui [ e . player_index ] 210if not self then 211return 212end 213self . search_query =string . lower ( e . text ) 214update_gui ( self ) 215end 216 217--- @param e EventData.on_gui_elem_changed 218local function on_divisor_elem_changed ( e ) 219local self =storage . gui [ e . player_index ] 220if not self then 221return 222end 223local entity_id =e . element . elem_value --[[@as SignalID?]] 224local timescale =self . selected_timescale 225local timescale_data =gui_util . timescale_data [ timescale ] 226if timescale_data . divisor_required and not entity_id then 227e . element . elem_value =self [ timescale_data . divisor_source ] 228return 229end 230self [ timescale_data . divisor_source ] =entity_id 231update_gui ( self ) 232end 233 234--- @param e EventData.on_gui_selection_state_changed 235local function on_timescale_dropdown_changed ( e ) 236local self =storage . gui [ e . player_index ] 237if not self then 238return 239end 240local new_timescale =gui_util . ordered_timescales [ e . element . selected_index ] 241self . selected_timescale =new_timescale 242update_gui ( self ) 243end 244 245--- @param e EventData.on_gui_text_changed 246local function on_multiplier_textfield_changed ( e ) 247local self =storage . gui [ e . player_index ] 248if not self then 249return 250end 251local text =e . text 252-- Don't prevent insertion of a decimal point or zeroes 253local last_char =string . sub ( text , # text ) 254if last_char == "." or ( string . match ( text , "%." ) and last_char == "0" ) then 255return 256end 257local new_value =tonumber ( text ) 258if not new_value or new_value == 0 then 259return 260end 261self . manual_multiplier =new_value 262update_gui ( self ) 263end 264 265--- @param e EventData.on_gui_click 266local function on_multiplier_nudge_clicked ( e ) 267local self =storage . gui [ e . player_index ] 268if not self then 269return 270end 271self . manual_multiplier =math . max ( 1 , math . floor ( self . manual_multiplier ) + e . element . tags . delta ) 272update_gui ( self ) 273end 274 275--- @param name string 276--- @param sprite SpritePath 277--- @param tooltip LocalisedString 278--- @param handler flib.GuiElemHandler 279--- @return flib.GuiElemDef 280local function frame_action_button ( name , sprite , tooltip , handler ) 281return { 282type ="sprite-button" , 283name =name , 284style ="frame_action_button" , 285sprite =sprite , 286tooltip =tooltip , 287mouse_button_filter ={ "left" } , 288handler ={ [ defines . events . on_gui_click ] =handler } , 289} 290end 291 292--- @param player LuaPlayer 293local function destroy_gui ( player ) 294if not storage . gui then 295return 296end 297local self =storage . gui [ player . index ] 298if not self then 299return 300end 301storage . gui [ player . index ] =nil 302local window =self . elems . rcalc_window 303if not window . valid then 304return 305end 306window . destroy () 307end 308 309--- @param player LuaPlayer 310--- @return GuiData 311local function build_gui ( player ) 312destroy_gui ( player ) 313 314local elems =flib_gui . add ( player . gui . screen , { 315type ="frame" , 316name ="rcalc_window" , 317direction ="vertical" , 318visible =false , 319handler ={ [ defines . events . on_gui_closed ] =on_window_closed } , 320{ 321type ="flow" , 322style ="flib_titlebar_flow" , 323drag_target ="rcalc_window" , 324handler ={ [ defines . events . on_gui_click ] =on_titlebar_click } , 325{ 326type ="label" , 327style ="flib_frame_title" , 328caption ={ "mod-name.RateCalculator" } , 329ignored_by_interaction =true , 330} , 331{ type ="empty-widget" , style ="flib_titlebar_drag_handle" , ignored_by_interaction =true } , 332{ 333type ="textfield" , 334name ="search_textfield" , 335style ="flib_titlebar_search_textfield" , 336visible =false , 337clear_and_focus_on_right_click =true , 338lose_focus_on_confirm =true , 339handler ={ [ defines . events . on_gui_text_changed ] =on_search_text_changed } , 340} , 341frame_action_button ( "search_button" , "utility/search" , { "gui.flib-search-instruction" } , on_search_button_click ), 342frame_action_button ( 343"nav_backward_button" , 344"flib_nav_backward_white" , 345{ "gui.rcalc-previous-set" } , 346on_nav_backward_button_click 347), 348frame_action_button ( 349"nav_forward_button" , 350"flib_nav_forward_white" , 351{ "gui.rcalc-next-set" } , 352on_nav_forward_button_click 353), 354frame_action_button ( "pin_button" , "flib_pin_white" , { "gui.flib-keep-open" } , on_pin_button_click ), 355frame_action_button ( "close_button" , "utility/close" , { "gui.close-instruction" } , on_close_button_click ), 356} , 357{ 358type ="frame" , 359style ="inside_shallow_frame" , 360direction ="vertical" , 361{ 362type ="frame" , 363style ="subheader_frame" , 364{ type ="label" , style ="subheader_caption_label" , caption ={ "gui.rcalc-timescale" } } , 365{ type ="empty-widget" , style ="flib_horizontal_pusher" } , 366{ 367type ="choose-elem-button" , 368name ="timescale_divisor_chooser" , 369style ="rcalc_units_choose_elem_button" , 370elem_type ="entity-with-quality" , 371tooltip ={ "gui.rcalc-capacity-divisor-description" } , 372handler ={ [ defines . events . on_gui_elem_changed ] =on_divisor_elem_changed } , 373} , 374{ 375type ="drop-down" , 376name ="timescale_dropdown" , 377items =flib_table . mapped ( gui_util . ordered_timescales , function ( timescale ) 378return { "string-mod-setting.rcalc-default-timescale-" .. timescale } 379end ), 380handler ={ [ defines . events . on_gui_selection_state_changed ] =on_timescale_dropdown_changed } , 381} , 382{ type ="label" , caption ="[img=quantity-multiplier]" } , 383{ 384type ="flow" , 385style ="rcalc_multiplier_holder_flow" , 386{ 387type ="textfield" , 388name ="multiplier_textfield" , 389style ="rcalc_multiplier_textfield" , 390numeric =true , 391allow_decimal =true , 392clear_and_focus_on_right_click =true , 393lose_focus_on_confirm =true , 394tooltip ={ "gui.rcalc-manual-multiplier-description" } , 395text ="1" , 396handler ={ [ defines . events . on_gui_text_changed ] =on_multiplier_textfield_changed } , 397} , 398{ 399type ="flow" , 400style ="rcalc_multiplier_nudge_buttons_flow" , 401direction ="vertical" , 402{ 403type ="sprite-button" , 404style ="rcalc_multiplier_nudge_button" , 405sprite ="rcalc_nudge_increase" , 406tooltip ="+1" , 407tags ={ delta =1 } , 408handler ={ [ defines . events . on_gui_click ] =on_multiplier_nudge_clicked } , 409} , 410{ 411type ="sprite-button" , 412style ="rcalc_multiplier_nudge_button" , 413sprite ="rcalc_nudge_decrease" , 414tooltip ="-1" , 415tags ={ delta =- 1 } , 416handler ={ [ defines . events . on_gui_click ] =on_multiplier_nudge_clicked } , 417} , 418} , 419} , 420} , 421{ 422type ="scroll-pane" , 423name ="rates_scroll_pane" , 424style ="rcalc_rates_table_scroll_pane" , 425{ type ="flow" , name ="rates_flow" , style ="rcalc_rates_table_horizontal_flow" } , 426} , 427{ 428type ="frame" , 429name ="errors_frame" , 430style ="rcalc_negative_subfooter_frame" , 431direction ="vertical" , 432visible =false , 433} , 434} , 435} ) 436 437player . opened =elems . rcalc_window 438 439local default_timescale =player . mod_settings [ "rcalc-default-timescale" ]. value --[[@as Timescale]] 440--- @type GuiData 441local self ={ 442display_data_lookup ={} , 443elems =elems , 444inserter_divisor =gui_util . get_first_prototype ( storage . elem_filters . inserter_divisor ), 445manual_multiplier =1 , 446pinned =false , 447player =player , 448search_open =false , 449search_query ="" , 450selected_set_index =0 , 451selected_timescale =default_timescale , 452sets ={} , 453transport_belt_divisor =gui_util . get_first_prototype ( storage . elem_filters . transport_belt_divisor ), 454} 455storage . gui [ player . index ] =self 456 457reset_location ( self ) 458 459return self 460end 461 462--- @param e EventData.on_runtime_mod_setting_changed 463local function on_runtime_mod_setting_changed ( e ) 464if not string . match ( e . setting , "^rcalc" ) then 465return 466end 467local self =storage . gui [ e . player_index ] 468if not self then 469return 470end 471update_gui ( self ) 472if e . setting == "rcalc-default-gui-location" then 473reset_location ( self ) 474end 475end 476 477--- @param e EventData.CustomInputEvent 478local function on_linked_focus_search ( e ) 479local self =storage . gui [ e . player_index ] 480if not self or not self . elems . rcalc_window . valid or self . pinned or not self . elems . rcalc_window . visible then 481return 482end 483toggle_search ( self ) 484end 485 486local gui ={} 487 488--- @param player LuaPlayer 489--- @return CalculationSet? 490function gui . get_current_set ( player ) 491local self =storage . gui [ player . index ] 492if self then 493return self . sets [ self . selected_set_index ] 494end 495end 496 497--- @param player LuaPlayer 498--- @param set CalculationSet? 499--- @param new_selection boolean? 500function gui . build_and_show ( player , set , new_selection ) 501local self =storage . gui [ player . index ] 502if not self or not self . elems . rcalc_window . valid then 503self =build_gui ( player ) 504end 505local sets =self . sets 506if set and ( new_selection or not sets [ 1 ]) then 507sets [ # sets + 1 ] =set 508if # sets > 10 then 509table . remove ( sets , 1 ) 510end 511self . selected_set_index =# sets 512end 513if not sets [ self . selected_set_index ] then 514return 515end 516if new_selection then 517self . manual_multiplier =1 518end 519gui . show ( self ) 520end 521 522--- @param self GuiData 523function gui . show ( self ) 524update_gui ( self ) 525self . elems . rcalc_window . visible =true 526if not self . pinned then 527self . player . opened =self . elems . rcalc_window 528end 529self . elems . rcalc_window . bring_to_front () 530end 531 532function gui . on_init () 533--- @type table<uint, GuiData> 534storage . gui ={} 535 536gui_util . build_divisor_filters () 537gui_util . build_dictionaries () 538end 539 540function gui . on_configuration_changed () 541gui_util . build_divisor_filters () 542gui_util . build_dictionaries () 543 544for _ , player in pairs ( game . players ) do 545destroy_gui ( player ) 546end 547end 548 549gui . events ={ 550[ defines . events . on_runtime_mod_setting_changed ] =on_runtime_mod_setting_changed , 551[ "rcalc-linked-focus-search" ] =on_linked_focus_search , 552} 553 554flib_gui . add_handlers ( { 555on_close_button_click =on_close_button_click , 556on_divisor_elem_changed =on_divisor_elem_changed , 557on_multiplier_nudge_clicked =on_multiplier_nudge_clicked , 558on_multiplier_textfield_changed =on_multiplier_textfield_changed , 559on_nav_backward_button_click =on_nav_backward_button_click , 560on_nav_forward_button_click =on_nav_forward_button_click , 561on_pin_button_click =on_pin_button_click , 562on_search_button_click =on_search_button_click , 563on_search_text_changed =on_search_text_changed , 564on_timescale_dropdown_changed =on_timescale_dropdown_changed , 565on_titlebar_click =on_titlebar_click , 566on_window_closed =on_window_closed , 567} ) 568 569return gui