z_mirror/RecipeBook

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

Caleb HeuerFix clobbering of a generic `id` tag namea949b6c

trunk
10.8 KiB363 linesraw
1local flib_dictionary = require("__flib__.dictionary")
2local flib_format = require("__flib__.format")
3local flib_gui = require("__flib__.gui")
4local researched = require("scripts.database.researched")
5local search_tree = require("scripts.database.search-tree")
6local util = require("scripts.util")
7
8--- @param context MainGuiContext
9--- @return SearchTree
10local function get_search_tree(context)
11  if context.search_materials_only then
12    return search_tree.materials
13  elseif context.grouping_mode == "all" then
14    return search_tree.all
15  elseif context.grouping_mode == "exclude-recipes" then
16    return search_tree.separate_recipes
17  else -- "none"
18    return search_tree.plain
19  end
20end
21
22--- @class SearchPane.Results
23--- @field first_group_with_hits string
24--- @field hits integer
25--- @field groups table<string, SearchPane.GroupResults>
26
27--- @class SearchPane.GroupResults
28--- @field filter_hits integer
29--- @field query_hits integer
30--- @field results table<SpritePath, SearchPane.Result>
31
32--- @class SearchPane.Result
33--- @field group string
34--- @field subgroup string
35--- @field is_hidden boolean
36--- @field is_researched boolean
37--- @field prototype GenericPrototype
38--- @field path SpritePath
39
40--- @class SearchPane
41--- @field textfield LuaGuiElement
42--- @field count_label LuaGuiElement
43--- @field group_buttons table<string, LuaGuiElement>
44--- @field results_pane LuaGuiElement
45--- @field dictionary_warning LuaGuiElement
46--- @field context MainGuiContext
47--- @field results SearchPane.Results?
48--- @field query string
49--- @field selected_group string
50--- @field selected_result SpritePath?
51local search_pane = {}
52local mt = { __index = search_pane }
53script.register_metatable("search_pane", mt)
54
55--- @type function?
56search_pane.on_result_clicked = nil
57
58--- @param parent LuaGuiElement
59--- @param context MainGuiContext
60function search_pane.build(parent, context)
61  -- TODO: Respect "lists everywhere" setting and make a list instead of a grid.
62  local outer = parent.add({
63    type = "frame",
64    name = "filter_outer_frame",
65    style = "inside_deep_frame",
66    direction = "vertical",
67    index = 1,
68  })
69
70  local subheader = outer.add({ type = "frame", style = "subheader_frame" })
71  subheader.style.horizontally_stretchable = true
72
73  local textfield = subheader.add({
74    type = "textfield",
75    name = "search_textfield",
76    lose_focus_on_confirm = true,
77    clear_and_focus_on_right_click = true,
78    tooltip = { "gui.flib-search-instruction" },
79    tags = flib_gui.format_handlers({ [defines.events.on_gui_text_changed] = search_pane.on_query_changed }),
80  })
81  textfield.add({
82    type = "label",
83    name = "placeholder",
84    caption = { "gui.rb-search-placeholder" },
85    ignored_by_interaction = true,
86  }).style.font_color =
87    { 0, 0, 0, 0.6 }
88  subheader.add({ type = "empty-widget", style = "flib_horizontal_pusher" })
89  local count_label = subheader.add({ type = "label" })
90  count_label.style.right_padding = 8
91
92  local dictionary_warning = outer.add({
93    type = "frame",
94    name = "filter_warning_frame",
95    style = "negative_subheader_frame",
96    visible = false,
97  })
98  dictionary_warning.style.bottom_margin = 0
99  dictionary_warning.style.horizontally_stretchable = true
100  dictionary_warning.add({
101    type = "label",
102    style = "bold_label",
103    caption = { "gui.rb-localised-search-unavailable" },
104  }).style.left_margin =
105    8
106
107  local groups_table = outer.add({ type = "table", style = "rb_filter_group_button_table", column_count = 6 })
108
109  local results_pane = outer
110    .add({ type = "frame", style = "rb_filter_frame" })
111    .add({ type = "frame", style = "rb_filter_deep_frame" })
112    .add({ type = "scroll-pane", style = "rb_filter_scroll_pane", vertical_scroll_policy = "always" })
113
114  local tree = get_search_tree(context)
115
116  --- @type table<string, LuaGuiElement>
117  local group_buttons = {}
118  for group_name in pairs(tree.groups) do
119    group_buttons[group_name] = groups_table.add({
120      type = "sprite-button",
121      name = group_name,
122      style = "rb_filter_group_button_tab",
123      sprite = "item-group/" .. group_name,
124      elem_tooltip = { type = "item-group", name = group_name },
125      tags = flib_gui.format_handlers({ [defines.events.on_gui_click] = search_pane.on_group_clicked }),
126    })
127  end
128
129  --- @type SearchPane
130  local self = {
131    textfield = textfield,
132    count_label = count_label,
133    group_buttons = group_buttons,
134    results_pane = results_pane,
135    dictionary_warning = dictionary_warning,
136    context = context,
137    query = "",
138    selected_group = next(group_buttons) --[[@as string]],
139  }
140  setmetatable(self, mt)
141  self:update()
142  return self
143end
144
145function search_pane:update_results()
146  local profiler = game.create_profiler()
147
148  local to_lower_func = helpers.compare_versions(helpers.game_version, "2.0.67") >= 0 and helpers.multilingual_to_lower
149    or string.lower
150
151  local show_hidden = self.context.show_hidden
152  local show_unresearched = self.context.show_unresearched
153  local query = to_lower_func(self.query)
154  local force_index = self.context.player.force.index
155  local search_strings = flib_dictionary.get(self.context.player.index, "search") or {}
156
157  self.dictionary_warning.visible = next(search_strings) == nil
158
159  self.textfield.placeholder.visible = #query == 0
160
161  local tree = get_search_tree(self.context)
162
163  --- @type SearchPane.Results
164  local results = {
165    first_group_with_hits = "",
166    hits = 0,
167    groups = {},
168  }
169
170  for group_name, group in pairs(tree.groups) do
171    --- @type SearchPane.GroupResults
172    local group_results = {
173      filter_hits = 0,
174      query_hits = 0,
175      results = {},
176    }
177    results.groups[group_name] = group_results
178
179    for subgroup_name, subgroup in pairs(group) do
180      for _, prototype in pairs(subgroup) do
181        local is_hidden = prototype.hidden or prototype.hidden_in_factoriopedia
182        local is_researched = researched.is(prototype, force_index)
183        local filters_match = (show_hidden or not is_hidden) and (show_unresearched or is_researched)
184        if not filters_match then
185          goto continue
186        end
187
188        group_results.filter_hits = group_results.filter_hits + 1
189
190        local path = util.get_path(prototype)
191
192        local query_match = #query == 0
193        if not query_match then
194          local comp = search_strings[path] or string.gsub(path, "-", " ")
195          query_match = string.find(to_lower_func(comp), query, 1, true) ~= nil
196        end
197        if not query_match then
198          goto continue
199        end
200
201        group_results.query_hits = group_results.query_hits + 1
202
203        group_results.results[path] = {
204          button = nil,
205          group = group_name,
206          subgroup = subgroup_name,
207          is_hidden = is_hidden,
208          is_researched = is_researched,
209          prototype = prototype,
210          path = path,
211        }
212
213        ::continue::
214      end
215    end
216
217    results.hits = results.hits + group_results.query_hits
218    if group_results.query_hits > 0 and results.first_group_with_hits == "" then
219      results.first_group_with_hits = group_name
220    end
221  end
222
223  self.results = results
224
225  profiler.stop()
226  log({ "", "Update Results ", profiler })
227end
228
229function search_pane:update_gui()
230  local profiler = helpers.create_profiler()
231
232  local results = self.results
233  if not results then
234    return
235  end
236
237  local selected_group = self.selected_group
238
239  if selected_group == "" or results.groups[selected_group].query_hits == 0 then
240    selected_group = results.first_group_with_hits
241  end
242
243  self.count_label.caption = { "gui.rb-count-results", flib_format.number(results.hits) }
244
245  local group_buttons = self.group_buttons
246  for group_name, group_results in pairs(results.groups) do
247    local button = group_buttons[group_name]
248    button.visible = group_results.filter_hits > 0
249    button.enabled = group_results.query_hits > 0
250    button.toggled = selected_group == group_name
251    button.tooltip = { "gui.rb-count-results", flib_format.number(group_results.query_hits) }
252  end
253
254  local results_pane = self.results_pane
255  results_pane.clear()
256
257  if results.hits == 0 then
258    local no_results_frame = results_pane.add({
259      type = "frame",
260      name = "filter_no_results_label",
261      style = "negative_subheader_frame",
262    })
263    no_results_frame.style.horizontally_stretchable = true
264    no_results_frame.add({
265      type = "label",
266      style = "rb_subheader_bold_label",
267      caption = { "", "[img=warning-white] ", { "gui.nothing-found" } },
268    })
269  end
270
271  local group_results = results.groups[selected_group]
272  if not group_results then
273    return
274  end
275
276  local selected_result = self.selected_result
277
278  --- @type string?
279  local current_subgroup
280  --- @type LuaGuiElement?
281  local current_table
282  for result_path, result in pairs(group_results.results) do
283    if result.subgroup ~= current_subgroup then
284      current_table = results_pane.add({ type = "table", style = "slot_table", column_count = 10 })
285      current_subgroup = result.subgroup
286    end
287    --- @cast current_table -?
288
289    local style = "flib_slot_button_default"
290    if result.is_hidden then
291      style = "flib_slot_button_grey"
292    elseif not result.is_researched then
293      style = "flib_slot_button_red"
294    end
295    local result_id = util.get_id(result.prototype)
296    local button = current_table.add({
297      type = "sprite-button",
298      style = style,
299      sprite = result_path,
300      elem_tooltip = result_id --[[@as ElemID]],
301      tooltip = { "", { "gui.rb-control-hint" }, "\n", { "gui.rb-pipette-control-hint" } },
302      toggled = result_path == selected_result,
303      tags = flib_gui.format_handlers({
304        [defines.events.on_gui_click] = search_pane.on_result_clicked,
305      }, { rb_id = result_id }),
306    })
307    if result_path == selected_result then
308      results_pane.scroll_to_element(button)
309    end
310  end
311
312  profiler.stop()
313  log({ "", "Update GUI elements ", profiler })
314end
315
316function search_pane:update()
317  self:update_results()
318  self:update_gui()
319end
320
321--- @param prototype GenericPrototype?
322function search_pane:select_result(prototype)
323  if not prototype then
324    self.selected_result = nil
325    return
326  end
327  self.selected_result = util.get_path(prototype)
328  local group_name = prototype.group.name
329  local tree = get_search_tree(self.context)
330  if tree.groups[group_name] then
331    self.selected_group = prototype.group.name
332  end
333  self:update_gui()
334end
335
336function search_pane:focus_search()
337  self.textfield.select_all()
338  self.textfield.focus()
339end
340
341--- @private
342--- @param e EventData.on_gui_text_changed
343function search_pane:on_query_changed(e)
344  self.query = e.text
345  self:update()
346end
347
348--- @private
349--- @param e EventData.on_gui_text_changed
350function search_pane:on_group_clicked(e)
351  self.selected_group = e.element.name
352  self:update_gui()
353end
354
355flib_gui.add_handlers(search_pane, function(e, handler)
356  local main = storage.guis[e.player_index]
357  if not main or not main.window.valid then
358    return
359  end
360  handler(main.search_pane, e)
361end, "search_pane")
362
363return search_pane