aboutsummaryrefslogtreecommitdiff
path: root/option_set/option_set_list.gd
blob: 1ab753bf8da43c9d67d56747eddfeeda68443a3c (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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
extends Control
class_name OptionSetList

const POINTER_VELOCITY_DECAYING_FACTOR: float = PI
const POINTER_VELOCITY_BOOST_FACTOR: float = 1.25
const EXACT_SELECTION: bool = false

signal selection_changed	# (idx: int, text: String)

export var clear_signals_on_hide := true
export var autowrap			:= true
export var separation		:= 25

var v_scroll_bar			: VScrollBar
var sensor					: PointerInputSensor
var is_pointer_dragging		:= false
var pointer_drag_velocity	:= 0.0
var when_last_dragged		:= 0

var labels					: Control
var labels_end_positions	: Array
var labels_sizes			: Array

var items					: Array
var selected_idx			:= -1
var selected_item			:= ""
var is_dirty				:= true

var normal_style			: StyleBoxFlat
var selected_style			: StyleBoxFlat
var border_size				:= 5

onready var font			: Font = get_font("font")
onready var screen_dpcm		: float = float(OS.get_screen_dpi()) / 2.54

func _init():
	self.anchor_right			= 1.0
	self.anchor_bottom			= 1.0
	self.rect_clip_content		= true
	self.connect("hide", self, "_clear_signals")
		
	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)
	
	v_scroll_bar					= VScrollBar.new()
	v_scroll_bar.anchor_left		= 1.0
	v_scroll_bar.anchor_bottom		= 1.0
	v_scroll_bar.grow_horizontal	= Control.GROW_DIRECTION_BEGIN
	v_scroll_bar.name				= "v_scroll_bar"
	add_child(v_scroll_bar)
	
	sensor						= PointerInputSensor.new()
	sensor.anchor_right			= 1.0
	sensor.anchor_bottom		= 1.0
	sensor.name					= "sensor"
	add_child(sensor)
		
	connect("resized", self, "mark_as_dirty")
	
	sensor.connect("on_press", self, "pointer_input_on_press_handler")
	sensor.connect("on_drag", self, "pointer_input_on_drag_handler")
	sensor.connect("on_end_drag", self, "pointer_input_on_end_drag_handler")
	sensor.connect("on_click", self, "pointer_input_on_click_handler")
	sensor.connect("on_scroll", self, "pointer_input_on_scroll_handler")


func _ready():
	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()
	var bg_color = get_stylebox("pressed", "Button").bg_color
	selected_style.border_width_bottom = border_size
	selected_style.border_color = bg_color.lightened(0.333)
	selected_style.bg_color = bg_color


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()
	selected_idx = -1
	selected_item = ""
	v_scroll_bar.value = 0.0
	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_CENTER
		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 + separation
		
		position += height
		labels_end_positions.append(position)
		labels_sizes.append(height)


func _process(delta):
	
	if is_dirty:
		build_labels()
		is_dirty = false
	
	var viewable_ratio := 1.0
	var viewable_height := labels.rect_size.y
	if items.size() > 0:
		viewable_ratio = viewable_height / float(labels_end_positions.back())
	
	v_scroll_bar.min_value = 0
	v_scroll_bar.max_value = viewable_height / viewable_ratio
	v_scroll_bar.visible = viewable_ratio < 1.0
	v_scroll_bar.page = viewable_height
	
	var labels_offset := v_scroll_bar.value
	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
	
	var right_margin = - v_scroll_bar.rect_size.x if v_scroll_bar.visible else 0.0
	labels.margin_right = right_margin
	sensor.margin_right = right_margin
	
	# Apply drag movement inertia.
	if is_pointer_dragging == false && abs(pointer_drag_velocity) > 0.5:
		pointer_drag_velocity *= clamp((1.0 - POINTER_VELOCITY_DECAYING_FACTOR * delta), 0.0, 1.0)
		v_scroll_bar.value -= pointer_drag_velocity * delta


func get_item_at_position(mouse_position: Vector2) -> int:
	var labels_offset		:= v_scroll_bar.value
	var position			:= mouse_position.y + labels_offset
	var item_idx			:= labels_end_positions.bsearch(position)
	
	if item_idx == items.size():
		item_idx = -1
	return item_idx


func select(index: int):
	selected_idx = index
	selected_item = items[selected_idx] if selected_idx >= 0 && selected_idx < items.size() else ""
	emit_signal("selection_changed", selected_idx, selected_item)


func unselect():
	select(-1)


func pointer_input_on_press_handler(pointer: PointerInputSensor.PointerInputData):
	is_pointer_dragging = true
	grab_focus()


func pointer_input_on_drag_handler(pointer: PointerInputSensor.PointerInputData):
	is_pointer_dragging = true
	var reported_velocity_abs := abs(pointer.velocity.y)
	var relative_velocity := pointer.relative_position.y * Engine.get_frames_per_second()
	var relative_velocity_abs := abs(relative_velocity)
	pointer_drag_velocity = pointer.velocity.y if reported_velocity_abs > relative_velocity_abs else relative_velocity
	v_scroll_bar.value -= pointer.relative_position.y
	when_last_dragged = OS.get_ticks_msec()


func pointer_input_on_end_drag_handler(pointer: PointerInputSensor.PointerInputData):
	is_pointer_dragging = false
	if OS.get_ticks_msec() - when_last_dragged > 20:
		pointer_drag_velocity = 0.0
	else:
		pointer_drag_velocity *= POINTER_VELOCITY_BOOST_FACTOR


func pointer_input_on_click_handler(pointer: PointerInputSensor.PointerInputData):
	var item_idx := get_item_at_position(pointer.current_position - rect_global_position)
	select(item_idx)


func pointer_input_on_scroll_handler(pointer: PointerInputSensor.PointerInputData):
	pointer_drag_velocity -= pointer.scroll * POINTER_VELOCITY_BOOST_FACTOR * screen_dpcm


func _clear_signals():
	if clear_signals_on_hide == false:
		return
	
	for signal_name in ["selection_changed"]:
		for it in get_signal_connection_list(signal_name):
			disconnect(it.signal, it.target, it.method)