diff options
Diffstat (limited to 'option_set/option_set_list.gd')
| -rw-r--r-- | option_set/option_set_list.gd | 293 |
1 files changed, 189 insertions, 104 deletions
diff --git a/option_set/option_set_list.gd b/option_set/option_set_list.gd index 3cff78d..cad9e9c 100644 --- a/option_set/option_set_list.gd +++ b/option_set/option_set_list.gd @@ -1,93 +1,115 @@ 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") -var vscrollbar: VScrollBar -var labels: Control -var labels_positions: Array -var labels_sizes: Array -var font: Font - -var items: Array = [ - "item 1", - "item 22", - "item 333", - "item 4444", - "item 55555", - "item 666666", - "item 7777777", - "item 88888888", - "item 999999999", - "This is the longest item of all, but eventually stops.", - "item 1", - "item 22", - "item 333", - "item 4444", - "item 55555", - "item 666666", - "item 7777777", - "item 88888888", - "item 999999999", - "This is the longest item of all, but eventually stops.", - "item 1", - "item 22", - "item 333", - "item 4444", - "item 55555", - "item 666666", - "item 7777777", - "item 88888888", - "item 999999999", - "This is the longest item of all, but eventually stops.", -] - -# @DAM List of ideas to implement on this element: -# - Allow to toggle word-wrap on or off; -# - Allow to change items; -# - Only build and process labels when needed; 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.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" + vscrollbar.grow_horizontal = Control.GROW_DIRECTION_BEGIN + vscrollbar.name = "vscrollbar" add_child(vscrollbar) - font = get_font("font") - connect("resized", self, "build_labels") + 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") -# @DAM We connect build_labels to resized during init. This calls build_labels immediately and the -# labels.rect_size is still not properly set thus, making the max_required_labels to return an -# incorrect value. To patch this, we are calling build_labels again on the _ready. Maybe we can sort -# this out in a cleaner way? -func _ready(): - build_labels() +func mark_as_dirty(): + is_dirty = true +func add_item(text: String): + items.append(text) + is_dirty = true -# @DAM This is only used once so, why not inline it? -func max_required_labels() -> int: - var max_labels := ceil(labels.rect_size.y / get_line_height()) + 1 - return int(min(items.size(), max_labels)) +func add_items(texts: Array): + items.append_array(texts) + is_dirty = true -func get_line_height() -> float: - return font.get_height() + font.get_descent() +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 build_labels(): - var new_max_required_labels := max_required_labels() - var delta := new_max_required_labels - labels.get_child_count() +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 = true - label.anchor_left = 0.0 + label.autowrap = autowrap label.anchor_right = 1.0 label.valign = Label.VALIGN_TOP labels.add_child(label) @@ -98,61 +120,124 @@ func build_labels(): labels.remove_child(label) label.free() delta += 1 - - -func process_labels(): - labels_positions.clear() + + labels_end_positions.clear() labels_sizes.clear() - var position := 0.0 - var limit := labels.rect_size.x + + 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: - var size := font.get_wordwrap_string_size(it, limit).y + 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 - # The get_wordwrap_string_size does not include the vertical spacing for the last line, thus - # we need to include it manually. - # Method 1 -# var lines = size / font.get_height() -# var height = size + font.get_descent() * lines - # Method 2 - var height = get_line_height() * ceil(size / get_line_height()) - # Method 3 - not correct -# var height = size + font.get_descent() - position += height - labels_positions.append(position) + labels_end_positions.append(position) labels_sizes.append(height) func _process(delta): - # @DAM We are recalculating the labels size and position on every update. - # This only has to be done on resize or when items change. - process_labels() + 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 = rect_size.y - var ratio := rect_size.y / float(labels_positions[labels_positions.size()-1]) + vscrollbar.max_value = labels.rect_size.y vscrollbar.visible = ratio < 1.0 vscrollbar.page = ratio * (vscrollbar.max_value - vscrollbar.min_value) - var offset := vscrollbar.value - var bs_value := offset / ratio - var offset_idx := labels_positions.bsearch(bs_value) - - var wasted := 0 # @DAM To be removed. - var idx := 0 + 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 + offset_idx >= items.size(): - label.text = "" - wasted += 1 + + if idx >= items.size(): + label.visible = false continue -# break # @DAM Or should we use continue? - label.text = items[idx + offset_idx] - label.rect_position.y = labels_positions[idx + offset_idx] - labels_sizes[idx + offset_idx] - bs_value + + 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 - if Engine.get_idle_frames() % 30 == 1: - print_debug("Wasted: %s" % wasted) - 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) + + |
