aboutsummaryrefslogtreecommitdiff
path: root/option_set/option_set_list.gd
blob: cad9e9c3b7f1c7bc72a0416fbd15fa0abed8297b (plain)
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)