1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
|
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)
|