z_mirror/RateCalculator

git clone https://git.raiguard.me/z_mirror/RateCalculator

Caleb HeuerRevert back to previous dictionary logic75c18ca

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