Files
infinitefusion-e18/Data/Scripts/016_UI/025_MultiSelectOptionsMenu.rb
T
2026-11-02 11:34:46 -05:00

456 lines
13 KiB
Ruby

class MultiSelectOptionsScene
attr_reader :selected_items
attr_reader :canceled
SWITCH_FOCUS_BUTTONS = [Input::L, Input::R]
CURSOR_X_OFFSET = -4
ARROW_NORMAL = "Graphics/Pictures/selarrow"
ARROW_ACTIVE = "Graphics/Pictures/selarrow_red"
def initialize(items, preselected = [], title = "Select options",
item_name_proc = nil, filter_placeholder = "Shift to search",
item_weight_proc = nil)
@items = items
@item_name_proc = item_name_proc || proc { |item| item.to_s }
@item_weight_proc = item_weight_proc || proc { |_item| 1 }
@selected_items = preselected.dup
@selected_display_items = []
@focus = :full # :full or :selected
@title_text = title
@filter_placeholder = filter_placeholder
@canceled = false
end
def pbStartScene(window_x = 0, window_y = 0)
title_height = 60
filter_height = 60
left_width = Graphics.width / 2
right_x = window_x + left_width
right_width = Graphics.width - left_width
reference_text = @items.map { |i| @item_name_proc.call(i) }.max_by(&:length) || @filter_placeholder
reference_text = @filter_placeholder if @filter_placeholder.length > reference_text.length
@viewport = Viewport.new(0, 0, Graphics.width, Graphics.height)
@viewport.z = 99999
@sprites = {}
@sprites["title"] = Window_UnformattedTextPokemon.newWithSize(
_INTL(@title_text), window_x, window_y, Graphics.width, title_height, @viewport)
@sprites["filter"] = Window_UnformattedTextPokemon.newWithSize(
reference_text, window_x, window_y + title_height, left_width, filter_height, @viewport)
@sprites["filter"].setTextToFit(reference_text)
@sprites["option"] = Window_MultiSelectFilterList.new(
[], window_x, window_y + title_height + filter_height,
left_width, Graphics.height - (window_y + title_height + filter_height),
@viewport
)
@sprites["option"].active = true
pbSetSystemFont(@sprites["option"].contents)
# Right-side panel: selected items + running total
@sprites["selected_title"] = Window_UnformattedTextPokemon.newWithSize(
"", right_x, window_y + title_height, right_width, filter_height, @viewport)
@sprites["selected_list"] = Window_ReadOnlyList.new(
[], right_x, window_y + title_height + filter_height,
right_width, Graphics.height - (window_y + title_height + filter_height),
@viewport
)
@sprites["selected_list"].active = false
@sprites["selected_list"].index = 0
@arrow_paths = {}
@sprites["cursor"] = IconSprite.new(0, 0, @viewport)
@sprites["cursor"].setBitmap(ARROW_ACTIVE)
@arrow_paths[@sprites["cursor"]] = ARROW_ACTIVE
@sprites["cursor"].z = 99999
@sprites["cursor2"] = IconSprite.new(0, 0, @viewport)
@sprites["cursor2"].setBitmap(ARROW_NORMAL)
@arrow_paths[@sprites["cursor2"]] = ARROW_NORMAL
@sprites["cursor2"].z = 99999
@filterText = ""
@filteredItems = []
refreshList
end
# ---------------------------------------------------------------- cursors
def updateCursorPosition
positionArrow(@sprites["cursor"], @sprites["option"], @focus == :full)
positionArrow(@sprites["cursor2"], @sprites["selected_list"], @focus == :selected)
@sprites["cursor2"].visible = false if @selected_items.empty?
end
def positionArrow(arrow, win, focused)
rect = win.itemRect(win.index)
if rect.width == 0 && rect.height == 0 # index out of visible range
arrow.visible = false
return
end
# Red arrow for the active list, normal arrow for the other
path = focused ? ARROW_ACTIVE : ARROW_NORMAL
if @arrow_paths[arrow] != path
arrow.setBitmap(path)
@arrow_paths[arrow] = path
end
arrow_height = arrow.bitmap ? arrow.bitmap.height : 0
y_center_offset = (win.rowHeight - arrow_height) / 2
arrow.x = win.x + win.startX + rect.x + CURSOR_X_OFFSET
arrow.y = win.y + win.startY + rect.y + y_center_offset
arrow.opacity = focused ? 255 : 110
arrow.visible = true
end
# ---------------------------------------------------------------- focus
def setFocus(focus)
focus = :full if focus == :selected && @selected_items.empty?
@focus = focus
@sprites["option"].active = (@focus == :full)
@sprites["selected_list"].active = (@focus == :selected)
syncFullListToSelected if @focus == :selected
updateCursorPosition
end
def toggleFocus
pbSEPlay("GUI naming tab swap start")
setFocus(@focus == :full ? :selected : :full)
end
def focusLatestSelected
@sprites["selected_list"].index = 0
updateCursorPosition
end
def syncFullListToSelected
item = @selected_display_items[@sprites["selected_list"].index]
return if item.nil?
idx = @filteredItems.index(item)
return if idx.nil? # item is hidden by the current search filter
@sprites["option"].index = itemStartIndex + idx
updateCursorPosition
end
def removeFromSelected(list_index)
item = @selected_display_items[list_index]
return if item.nil?
@selected_items.delete(item)
pbSEPlay("GUI naming tab swap end")
keep_index = @sprites["selected_list"].index
refreshList
max_index = [@selected_items.length - 1, 0].max
@sprites["selected_list"].index = [keep_index, max_index].min
setFocus(:full)
idx = @filteredItems.index(item)
@sprites["option"].index = itemStartIndex + idx if idx
updateCursorPosition
end
# ---------------------------------------------------------------- indices
def cancelIndex; return 0; end
def selectAllIndex; return 1; end
def confirmIndex; return 2; end
def itemStartIndex; return 3; end
# ---------------------------------------------------------------- refresh
def refreshList
filtered = @filterText.empty? ? @items.dup : @items.select { |i|
@item_name_proc.call(i).downcase.include?(@filterText.downcase)
}
@filteredItems = filtered
display = []
flags = []
display.push(_INTL("Cancel"))
flags.push(false)
all_selected = filtered.length > 0 && filtered.all? { |i| @selected_items.include?(i) }
display.push(all_selected ? _INTL("Unselect All") : _INTL("Select All"))
flags.push(false)
display.push(_INTL("Confirm"))
flags.push(false)
filtered.each do |i|
display.push(@item_name_proc.call(i))
flags.push(@selected_items.include?(i))
end
old_index = @sprites["option"].index || 0
@sprites["option"].commands = display
@sprites["option"].selected_flags = flags
@sprites["option"].confirm_index = confirmIndex
@sprites["option"].selall_index = selectAllIndex
@sprites["option"].cancel_index = cancelIndex
@sprites["option"].index = [old_index, display.length - 1].min
@sprites["option"].refresh
@sprites["filter"].text = @filterText.empty? ? @filter_placeholder : @filterText
refreshSelectedPanel
updateCursorPosition
end
def refreshSelectedPanel
total_weight = @selected_items.sum { |i| @item_weight_proc.call(i) }
@sprites["selected_title"].text = _INTL("{1} selected sprites", total_weight)
@selected_display_items = @selected_items.reverse
lines = @selected_display_items.map { |i| @item_name_proc.call(i) }
lines = [_INTL("")] if lines.empty?
old_index = @sprites["selected_list"].index || 0
@sprites["selected_list"].commands = lines
@sprites["selected_list"].index = [old_index, lines.length - 1].min
@sprites["selected_list"].refresh
end
def pbUpdate
pbUpdateSpriteHash(@sprites)
end
# ---------------------------------------------------------------- main loop
def pbOptions
loop do
Graphics.update
Input.update
if SWITCH_FOCUS_BUTTONS.any? { |b| Input.trigger?(b) }
toggleFocus
next
end
if Input.trigger?(Input::ACTION)
pbStartSearchEntry
next
end
if Input.trigger?(Input::BACK)
@canceled = true
break
end
if @focus == :selected
old_index = @sprites["selected_list"].index
@sprites["selected_list"].update
syncFullListToSelected if @sprites["selected_list"].index != old_index
updateCursorPosition
if Input.trigger?(Input::USE)
removeFromSelected(@sprites["selected_list"].index)
end
next
end
# ---- full list focused ----
old_index = @sprites["option"].index
@sprites["option"].update
updateCursorPosition if @sprites["option"].index != old_index
if Input.repeat?(Input::RIGHT)
pbSEPlay("GUI party switch")
scroll_amount = 8
last = @sprites["option"].itemCount - 1
if @sprites["option"].index + scroll_amount >= last
@sprites["option"].index = last
else
@sprites["option"].index += scroll_amount
end
updateCursorPosition
next
end
if Input.repeat?(Input::LEFT)
pbSEPlay("GUI party switch")
scroll_amount = -8
if @sprites["option"].index + scroll_amount <= 0
scrollToConfirm
else
@sprites["option"].index += scroll_amount
end
next
end
if Input.trigger?(Input::USE)
index = @sprites["option"].index
if index == cancelIndex
@canceled = true
break
elsif index == selectAllIndex
all_selected = @filteredItems.length > 0 && @filteredItems.all? { |i| @selected_items.include?(i) }
if all_selected
@selected_items -= @filteredItems
else
@filteredItems.each do |item|
@selected_items << item unless @selected_items.include?(item)
end
end
pbSEPlay("GUI naming confirm")
refreshList
focusLatestSelected
elsif index == confirmIndex
break
else
item = @filteredItems[index - itemStartIndex]
if @selected_items.include?(item)
@selected_items.delete(item)
pbSEPlay("GUI naming tab swap end")
else
@selected_items << item
pbSEPlay("GUI naming confirm")
end
refreshList
focusLatestSelected
end
end
end
end
def scrollToConfirm
@sprites["option"].index = confirmIndex
updateCursorPosition
end
def pbStartSearchEntry
if $PokemonSystem.textinput == 1 # keyboard
scene = PokedexTextEntry.new
else
scene = PokemonEntryScene2.new
end
scene.pbStartScene(
_INTL("Search:"),
0, # min length (0 allows clearing the filter)
30, # max length
@filterText
)
query = scene.pbEntry
scene.pbEndScene
if query
@filterText = query
refreshList
syncFullListToSelected if @focus == :selected
end
end
def pbEndScene
pbPlayCloseMenuSE
pbDisposeSpriteHash(@sprites)
@viewport.dispose
end
end
class MultiSelectOptionScreen
def initialize(scene)
@scene = scene
end
def pbStartScreen(window_x = 0, window_y = 0)
@scene.pbStartScene(window_x, window_y)
@scene.pbOptions
@scene.pbEndScene
return nil if @scene.canceled
return @scene.selected_items
end
end
class Window_MultiSelectFilterList < Window_DrawableCommand
attr_accessor :commands
attr_accessor :selected_flags
attr_accessor :confirm_index
attr_accessor :selall_index
attr_accessor :cancel_index
def initialize(commands, x, y, width, height, viewport = nil)
@commands = commands
@selected_flags = []
@confirm_index = nil
@cancel_index = nil
@selall_index = nil
@selBaseColor = Color.new(48, 96, 216)
@selShadowColor = Color.new(32, 32, 32)
@confirmBaseColor = Color.new(64, 200, 96)
@confirmShadowColor = Color.new(24, 96, 40)
@cancelBaseColor = Color.new(216, 64, 64)
@cancelShadowColor = Color.new(96, 24, 24)
super(x, y, width, height, viewport)
end
def itemCount
return @commands.length
end
def drawCursor(index, rect)
return Rect.new(rect.x + 16, rect.y, rect.width - 16, rect.height)
end
def drawItem(index, _count, rect)
rect = drawCursor(index, rect)
text = @commands[index]
if index == @confirm_index
base, shadow = @confirmBaseColor, @confirmShadowColor
elsif index == @selall_index
base, shadow = @selBaseColor, @selShadowColor
elsif index == @cancel_index
base, shadow = @cancelBaseColor, @cancelShadowColor
elsif @selected_flags[index]
base, shadow = @selBaseColor, @selShadowColor
else
base, shadow = self.baseColor, self.shadowColor
end
pbDrawShadowText(self.contents, rect.x, rect.y, rect.width, rect.height, text, base, shadow)
end
def refresh
self.contents.clear if self.contents
super
end
end
# Used for the "Selected" panel.
class Window_ReadOnlyList < Window_DrawableCommand
attr_accessor :commands
def initialize(commands, x, y, width, height, viewport = nil)
@commands = commands
super(x, y, width, height, viewport)
self.active = false
end
def itemCount
return @commands.length
end
def drawCursor(index, rect)
return Rect.new(rect.x + 16, rect.y, rect.width - 16, rect.height)
end
def drawItem(index, _count, rect)
rect = drawCursor(index, rect)
pbDrawShadowText(self.contents, rect.x, rect.y, rect.width, rect.height,
@commands[index], self.baseColor, self.shadowColor)
end
def refresh
self.contents.clear if self.contents
super
end
end