aboutsummaryrefslogtreecommitdiff
path: root/option_set/option_set_list.gd
diff options
context:
space:
mode:
Diffstat (limited to 'option_set/option_set_list.gd')
-rw-r--r--option_set/option_set_list.gd293
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)
+
+