extends Control class_name OptionSetList signal item_selected # (idx: int, text: String) signal nothing_selected # () export var autowrap := true var vscrollbar : VScrollBar var labels : Control var labels_end_positions: Array var labels_sizes : Array var items : Array var selected_idx := -1 var is_dirty := true var normal_style : StyleBoxFlat var selected_style : StyleBoxFlat var border_size := 5 onready var font : Font = get_font("font") func _init(): self.anchor_right = 1.0 self.anchor_bottom = 1.0 self.rect_clip_content = true labels = Control.new() labels.anchor_right = 1.0 labels.anchor_bottom = 1.0 labels.mouse_filter = Control.MOUSE_FILTER_IGNORE labels.name = "labels" add_child(labels) vscrollbar = VScrollBar.new() vscrollbar.anchor_left = 1.0 vscrollbar.anchor_bottom = 1.0 vscrollbar.grow_horizontal = Control.GROW_DIRECTION_BEGIN vscrollbar.name = "vscrollbar" add_child(vscrollbar) normal_style = StyleBoxFlat.new() normal_style.border_width_bottom = border_size normal_style.border_color = Color.white * 0.333 normal_style.bg_color = Color.transparent selected_style = StyleBoxFlat.new() selected_style.border_width_bottom = border_size selected_style.border_color = Color.white * 0.333 selected_style.bg_color = Color.dimgray connect("resized", self, "mark_as_dirty") func mark_as_dirty(): is_dirty = true func add_item(text: String): items.append(text) is_dirty = true func add_items(texts: Array): items.append_array(texts) is_dirty = true func set_item(index: int, text: String): items[index] = text is_dirty = true func set_items(indices: Array, texts: Array): for idx in indices.size(): items[indices[idx]] = texts[idx] is_dirty = true func remove_item(index: int): items.remove(index) is_dirty = true func remove_items(indices: Array): indices.sort() var idx := indices.size()-1 while idx >= 0: items.remove(indices[idx]) idx -= 1 is_dirty = true func clear_items(): items.clear() is_dirty = true func build_labels(): var num_of_labels := int(min( items.size(), ceil(labels.rect_size.y / (font.get_height() + border_size)) + 1 )) var delta := num_of_labels - labels.get_child_count() while delta > 0: var label := Label.new() label.autowrap = autowrap label.anchor_right = 1.0 label.valign = Label.VALIGN_TOP labels.add_child(label) delta -= 1 while delta < 0: var label := labels.get_child(0) as Label labels.remove_child(label) label.free() delta += 1 labels_end_positions.clear() labels_sizes.clear() if num_of_labels == 0: return var position := 0.0 var proto_label := labels.get_child(0) as Label var line_spacing := proto_label.get_constant("line_spacing") var line_height := proto_label.get_line_height() for it in items: proto_label.text = it var line_count := proto_label.get_line_count() var height := line_count * line_height + (line_count - 1) * line_spacing + border_size position += height labels_end_positions.append(position) labels_sizes.append(height) func _process(delta): if is_dirty: build_labels() is_dirty = false var ratio := 1.0 if items.size() > 0: ratio = vscrollbar.max_value / float(labels_end_positions.back()) vscrollbar.min_value = 0 vscrollbar.max_value = labels.rect_size.y vscrollbar.visible = ratio < 1.0 vscrollbar.page = ratio * (vscrollbar.max_value - vscrollbar.min_value) var scrollbar_offset := vscrollbar.value var labels_offset := scrollbar_offset / ratio var idx_offset := labels_end_positions.bsearch(labels_offset) var idx := idx_offset for label in labels.get_children(): if idx >= items.size(): label.visible = false continue label.visible = true label.text = items[idx] label.rect_size.y = labels_sizes[idx] label.rect_position.y = labels_end_positions[idx] - labels_sizes[idx] - labels_offset if idx == selected_idx: label.add_stylebox_override("normal", selected_style) else: label.add_stylebox_override("normal", normal_style) idx += 1 labels.margin_right = -vscrollbar.rect_size.x if vscrollbar.visible else 0.0 func get_item_at_position(mouse_position: Vector2) -> int: if items.size() == 0: return -1 var ratio := 1.0 if items.size() > 0: ratio = vscrollbar.max_value / float(labels_end_positions.back()) var scrollbar_offset := vscrollbar.value var labels_offset := scrollbar_offset / ratio var position := mouse_position.y + labels_offset var item_idx := labels_end_positions.bsearch(position) return int(min(item_idx, items.size()-1)) # Return last item when position is below it. func select(index: int): selected_idx = index emit_signal("item_selected", selected_idx, items[selected_idx]) func unselect(): selected_idx = -1 emit_signal("nothing_selected") #func _input(event: InputEvent): ## @DAM Test and debug code. Delete when ready. # if event is InputEventKey: # if event.pressed && event.scancode == KEY_C: # clear_items() # if event.pressed && event.scancode == KEY_F: # for idx in range(1, 26): # if idx % 10 == 0: # add_item("-- item %06d : This is the longest item of all, but eventually stops. --" % idx) # else: # add_item("-- item %06d --" % idx) func _gui_input(event: InputEvent): # @DAM A bug on GODOT-3.X makes events from non-mouse-emulated pointers (index > 0) unreliable. if event is InputEventScreenTouch || event is InputEventScreenDrag: if event.index != 0: return if event is InputEventMouseButton == false: return if event is InputEventScreenTouch && event.pressed == false: return var item_idx := get_item_at_position(get_local_mouse_position()) if item_idx >= 0 && item_idx < items.size(): select(item_idx)