diff options
| author | dam <dam@gudinoff> | 2022-04-10 08:32:49 +0000 |
|---|---|---|
| committer | dam <dam@gudinoff> | 2022-04-10 08:32:49 +0000 |
| commit | 2d7da6bbc23fb917dfe931eaeb5566da0a102ac3 (patch) | |
| tree | ae4d7541267dd1a7a5633ff79ac7de1e9055437b /ui | |
| parent | 75791aecbff0d8adc1011f45a69877cabff616e0 (diff) | |
| download | surgery-log-2d7da6bbc23fb917dfe931eaeb5566da0a102ac3.tar.zst surgery-log-2d7da6bbc23fb917dfe931eaeb5566da0a102ac3.zip | |
Code cleanup.
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/date_picker/date_picker.gd | 64 | ||||
| -rw-r--r-- | ui/date_picker/date_picker.tscn | 189 | ||||
| -rw-r--r-- | ui/date_picker/value_picker.gd | 132 | ||||
| -rw-r--r-- | ui/dialog/dialog.gd | 79 | ||||
| -rw-r--r-- | ui/modal_popup/modal_popup.gd | 76 | ||||
| -rw-r--r-- | ui/modal_popup/modal_popup.tscn | 40 | ||||
| -rw-r--r-- | ui/option_set/option_set.gd | 44 | ||||
| -rw-r--r-- | ui/option_set/option_set.tscn | 34 | ||||
| -rw-r--r-- | ui/option_set/option_set_list.gd | 294 | ||||
| -rw-r--r-- | ui/option_set/option_set_list.tscn | 9 | ||||
| -rw-r--r-- | ui/pointer_input_sensor.gd | 117 | ||||
| -rw-r--r-- | ui/touch_item_list/touch_item_list.gd | 68 | ||||
| -rw-r--r-- | ui/touch_item_list/touch_item_list.tscn | 19 | ||||
| -rw-r--r-- | ui/touch_vertical_container/touch_vertical_container.gd | 118 | ||||
| -rw-r--r-- | ui/touch_vertical_container/touch_vertical_container.tscn | 19 |
15 files changed, 1302 insertions, 0 deletions
diff --git a/ui/date_picker/date_picker.gd b/ui/date_picker/date_picker.gd new file mode 100644 index 0000000..e2a793f --- /dev/null +++ b/ui/date_picker/date_picker.gd @@ -0,0 +1,64 @@ +extends Control +class_name DatePicker + +const days_per_month: Dictionary = { + 1: 31, + 2: 28, + 3: 31, + 4: 30, + 5: 31, + 6: 30, + 7: 31, + 8: 31, + 9: 30, + 10: 31, + 11: 30, + 12: 31, +} + +onready var year_picker := get_node("year") as ValuePicker +onready var month_picker := get_node("month") as ValuePicker +onready var day_picker := get_node("day") as ValuePicker + + +func _process(delta: float): + var year := year_picker.value + var month := month_picker.value + var day := day_picker.value + var days_on_month: int = days_per_month[month] + + var is_leap_year := (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 + if is_leap_year && month == 2: + days_on_month = 29 + + if day > days_on_month: + day_picker.value = days_on_month + day_picker.max_value = days_on_month + + +func get_day() -> int: + return day_picker.value + + +func get_month() -> int: + return month_picker.value + + +func get_year() -> int: + return year_picker.value + + +func get_date() -> Dictionary: + return { + year = year_picker.value, + month = month_picker.value, + day = day_picker.value, + } + + +func set_date(new_year: int, new_month: int, new_day: int): + year_picker.value = new_year + month_picker.value = new_month + day_picker.value = new_day + + diff --git a/ui/date_picker/date_picker.tscn b/ui/date_picker/date_picker.tscn new file mode 100644 index 0000000..e3d88c4 --- /dev/null +++ b/ui/date_picker/date_picker.tscn @@ -0,0 +1,189 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/date_picker/value_picker.gd" type="Script" id=1] +[ext_resource path="res://ui/date_picker/date_picker.gd" type="Script" id=2] + +[node name="date_picker" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 2 ) + +[node name="year" type="Control" parent="."] +anchor_right = 0.333 +anchor_bottom = 1.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} +min_value = 1 +max_value = 9999 + +[node name="previous" type="Label" parent="year"] +anchor_right = 1.0 +anchor_bottom = 0.333 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="current" type="Label" parent="year"] +anchor_top = 0.333 +anchor_right = 1.0 +anchor_bottom = 0.666 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="input" type="LineEdit" parent="year/current"] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +focus_next = NodePath("../../../month/current/input") +custom_constants/minimum_spaces = 0 +align = 1 +max_length = 4 +caret_blink = true + +[node name="next" type="Label" parent="year"] +anchor_top = 0.666 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="month" type="Control" parent="."] +anchor_left = 0.333 +anchor_right = 0.666 +anchor_bottom = 1.0 +script = ExtResource( 1 ) +min_value = 1 +max_value = 12 + +[node name="previous" type="Label" parent="month"] +anchor_right = 1.0 +anchor_bottom = 0.333 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="current" type="Label" parent="month"] +anchor_top = 0.333 +anchor_right = 1.0 +anchor_bottom = 0.666 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="input" type="LineEdit" parent="month/current"] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +focus_next = NodePath("../../../day/current/input") +focus_previous = NodePath("../../../year/current/input") +custom_constants/minimum_spaces = 0 +align = 1 +max_length = 2 +caret_blink = true + +[node name="next" type="Label" parent="month"] +anchor_top = 0.666 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="day" type="Control" parent="."] +anchor_left = 0.666 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} +min_value = 1 +max_value = 31 + +[node name="previous" type="Label" parent="day"] +anchor_right = 1.0 +anchor_bottom = 0.333 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="current" type="Label" parent="day"] +anchor_top = 0.333 +anchor_right = 1.0 +anchor_bottom = 0.666 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="input" type="LineEdit" parent="day/current"] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +focus_previous = NodePath("../../../month/current/input") +custom_constants/minimum_spaces = 0 +align = 1 +max_length = 2 +caret_blink = true + +[node name="next" type="Label" parent="day"] +anchor_top = 0.666 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 1 +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="split_upper" type="ColorRect" parent="."] +anchor_top = 0.333 +anchor_right = 1.0 +anchor_bottom = 0.343 +color = Color( 1, 1, 1, 0.25 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="split_lower" type="ColorRect" parent="."] +anchor_top = 0.666 +anchor_right = 1.0 +anchor_bottom = 0.676 +color = Color( 1, 1, 1, 0.25 ) +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/ui/date_picker/value_picker.gd b/ui/date_picker/value_picker.gd new file mode 100644 index 0000000..b70f1aa --- /dev/null +++ b/ui/date_picker/value_picker.gd @@ -0,0 +1,132 @@ +extends Control + +class_name ValuePicker + +const VELOCITY_DECAYING_FACTOR: float = 5.5 +const BINNING_THRESHOLD: float = 0.75 +const BINNING_ADJUST_P: float = 5.0 +const DRAG_THRESHOLD_CM: float = 0.250 + +export var min_value: int +export var max_value: int + +var pointer: Dictionary +var anchor: float +var value: int +var offset: float + +var scroll_unit_height: float +var label_previous_base_position: float +var label_current_base_position: float +var label_next_base_position: float + +onready var input := get_node("current/input") as LineEdit +onready var label_previous := get_node("previous") as Label +onready var label_current := get_node("current") as Label +onready var label_next := get_node("next") as Label +onready var screen_dpcm := float(OS.get_screen_dpi()) / 2.54 + + +func _ready(): + pointer = { + index = -1, + initial_position = Vector2.ZERO, + current_position = Vector2.ZERO, + velocity = Vector2.ZERO, + was_dragged = false, + is_active = false, + } + + input.connect("text_entered", self, "input_text_entered") + input.connect("focus_entered", self, "input_focus_entered") + input.connect("focus_exited", self, "input_focus_exited") + + scroll_unit_height = label_current.rect_size.y + label_previous_base_position = label_previous.rect_position.y + label_current_base_position = label_current.rect_position.y + label_next_base_position = label_next.rect_position.y + + value = min_value + offset = 0.0 + + +func _process(delta: float): + + if pointer.is_active: + var dragged_distance: float = - (pointer.current_position.y - pointer.initial_position.y) + offset = anchor + (dragged_distance / scroll_unit_height) + value = int(offset) + offset -= value + else: + pointer.velocity *= clamp((1.0 - VELOCITY_DECAYING_FACTOR * delta), 0.0, 1.0) + offset -= pointer.velocity.y * delta / scroll_unit_height + if abs(pointer.velocity.y) < BINNING_THRESHOLD * scroll_unit_height: + offset -= offset * BINNING_ADJUST_P * delta + + # Using 'offset * 2.0' (equivalent to 'offset / 0.5') rounds the value based on 0.5 units. + var cummulative_displacement := int(offset * 2.0) + value = wrapi(value + cummulative_displacement, min_value, max_value + 1) + offset -= cummulative_displacement + + label_current.text = "%d" % value + label_next.text = "%d" % wrapi(value + 1, min_value, max_value + 1) + label_previous.text = "%d" % wrapi(value - 1, min_value, max_value + 1) + + var offset_position := offset * scroll_unit_height + label_current.rect_position.y = label_current_base_position - offset_position + label_previous.rect_position.y = label_previous_base_position - offset_position + label_next.rect_position.y = label_next_base_position - offset_position + + label_previous.modulate.a = 0.5 - offset + label_next.modulate.a = offset + 0.5 + + +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 InputEventScreenTouch && (pointer.is_active == false || pointer.index == event.index): + var touch := event as InputEventScreenTouch + pointer.is_active = event.pressed + pointer.current_position = touch.position + if pointer.is_active: + input.release_focus() + pointer.index = touch.index + pointer.initial_position = touch.position + anchor = value + offset + else: + if pointer.was_dragged == false: # Click detected. + input.grab_focus() + pointer.index = -1 + pointer.was_dragged = false + + if event is InputEventScreenDrag && event.index == pointer.index: + var drag := event as InputEventScreenDrag + pointer.current_position = drag.position + pointer.velocity = drag.speed + if pointer.current_position.distance_to(pointer.initial_position) / screen_dpcm > DRAG_THRESHOLD_CM: + pointer.was_dragged = true + + +func input_text_entered(new_text: String): + input.release_focus() + + +func input_focus_entered(): + pointer.velocity = Vector2.ZERO # Avoid changing to other value once entering input. + input.text = "%d" % value + input.visible = true + input.select_all() + label_current.self_modulate.a = 0.0 + + +func input_focus_exited(): + if input.text.is_valid_integer(): + value = wrapi(int(input.text), min_value, max_value + 1) + input.visible = false + label_current.self_modulate.a = 1.0 + + diff --git a/ui/dialog/dialog.gd b/ui/dialog/dialog.gd new file mode 100644 index 0000000..89fee5c --- /dev/null +++ b/ui/dialog/dialog.gd @@ -0,0 +1,79 @@ +extends Control +class_name Dialog + +signal answered # (accepted: bool) +signal accepted # () +signal rejected # () + +export var clear_signals_on_hide := true + +var message : Label +var accept_button : Button +var reject_button : Button + + +func _init(): + self.anchor_right = 1.0 + self.anchor_bottom = 1.0 + self.rect_clip_content = true + self.connect("hide", self, "_clear_signals") + + reject_button = Button.new() + reject_button.anchor_top = 1.0 + reject_button.anchor_left = 0.0 + reject_button.anchor_right = 0.5 + reject_button.margin_right = -5.0 + reject_button.rect_min_size.y = 62.0 + reject_button.grow_vertical = Control.GROW_DIRECTION_BEGIN + reject_button.name = "reject" + reject_button.connect("pressed", self, "_signal_rejected") + add_child(reject_button) + + accept_button = Button.new() + accept_button.anchor_top = 1.0 + accept_button.anchor_left = 0.5 + accept_button.anchor_right = 1.0 + accept_button.margin_left = 5.0 + accept_button.rect_min_size.y = 62.0 + accept_button.grow_vertical = Control.GROW_DIRECTION_BEGIN + accept_button.name = "accept" + accept_button.connect("pressed", self, "_signal_accepted") + add_child(accept_button) + + message = Label.new() + message.autowrap = true + message.align = Label.ALIGN_CENTER + message.anchor_right = 1.0 + message.anchor_bottom = 1.0 + add_child(message) + + +func _clear_signals(): + if clear_signals_on_hide == false: + return + + for signal_name in ["answered", "accepted", "rejected"]: + for it in get_signal_connection_list(signal_name): + disconnect(it.signal, it.target, it.method) + + +func _signal_rejected(): + emit_signal("rejected") + emit_signal("answered", false) + hide() + + +func _signal_accepted(): + emit_signal("accepted") + emit_signal("answered", true) + hide() + + +func setup(message: String, accept_label: String = "Accept", reject_label: String = "Reject"): + self.message.text = message + accept_button.visible = accept_label != "" + accept_button.text = accept_label + reject_button.visible = reject_label != "" + reject_button.text = reject_label + + diff --git a/ui/modal_popup/modal_popup.gd b/ui/modal_popup/modal_popup.gd new file mode 100644 index 0000000..95fc2c3 --- /dev/null +++ b/ui/modal_popup/modal_popup.gd @@ -0,0 +1,76 @@ +extends Control +class_name ModalPopup + +signal dismissed # () + +export var clear_signals_on_hide := true + +var control : Control +var control_parent : Node + +onready var title := get_node("title") as Label +onready var background := get_node("background") as Panel +onready var dismiss_button := get_node("dismiss") as Button + + +func _init(): + self.anchor_right = 1.0 + self.anchor_bottom = 1.0 + self.rect_clip_content = true + self.connect("hide", self, "_clear_signals") + + +func _ready(): + dismiss_button.connect("pressed", self, "dismiss") + + +func _clear_signals(): + if clear_signals_on_hide == false: + return + + for signal_name in ["dismissed"]: + for it in get_signal_connection_list(signal_name): + disconnect(it.signal, it.target, it.method) + + +func dismiss(): + emit_signal("dismissed") + close_popup() + + +func open_popup(title: String, item: Control): + if visible == true: + return + + self.title.text = title + + control = item + control.connect("hide", self, "close_popup") + control_parent = control.get_parent() + control_parent.remove_child(control) + self.add_child(control) + + control.anchor_left = background.anchor_left + control.anchor_top = background.anchor_top + control.anchor_right = background.anchor_right + control.anchor_bottom = background.anchor_bottom + control.margin_left = 20 + control.margin_top = 20 + control.margin_right = -20 + control.margin_bottom = -20 + + self.show() + control.show() + + +func close_popup(): + if visible == false: + return + + control.disconnect("hide", self, "close_popup") + control.hide() + self.hide() + remove_child(control) + control_parent.add_child(control) + control_parent = null + diff --git a/ui/modal_popup/modal_popup.tscn b/ui/modal_popup/modal_popup.tscn new file mode 100644 index 0000000..3814bef --- /dev/null +++ b/ui/modal_popup/modal_popup.tscn @@ -0,0 +1,40 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/modal_popup/modal_popup.gd" type="Script" id=1] +[ext_resource path="res://fonts/font_icons.tres" type="DynamicFont" id=2] + +[node name="popup" type="ColorRect"] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color( 0, 0, 0, 0.870588 ) +script = ExtResource( 1 ) + +[node name="background" type="Panel" parent="."] +anchor_left = 0.05 +anchor_top = 0.1 +anchor_right = 0.95 +anchor_bottom = 0.975 + +[node name="title" type="Label" parent="."] +anchor_left = 0.124 +anchor_top = 0.025 +anchor_right = 0.95 +anchor_bottom = 0.1 +margin_left = 0.0799866 +margin_right = -80.0 +margin_bottom = -20.0 +align = 1 +valign = 1 +autowrap = true + +[node name="dismiss" type="Button" parent="."] +anchor_left = 0.05 +anchor_top = 0.025 +anchor_right = 0.124 +anchor_bottom = 0.1 +margin_right = 0.0799866 +margin_bottom = -20.0 +custom_fonts/font = ExtResource( 2 ) +text = "" +flat = true diff --git a/ui/option_set/option_set.gd b/ui/option_set/option_set.gd new file mode 100644 index 0000000..25ca0ff --- /dev/null +++ b/ui/option_set/option_set.gd @@ -0,0 +1,44 @@ +extends Control +class_name OptionSet + +export var placeholder: String + +var text: String setget set_text, get_text + +func set_text(var value: String): + input.text = value + +func get_text() -> String: + return input.text + +var selected_idx: int + +onready var input := get_node("input") as LineEdit +onready var button := get_node("button") as Button +onready var popup := get_node("/root/main/popup") as ModalPopup +onready var options := get_node("/root/main/option_set_list") as OptionSetList + + +func _ready(): + assert(popup != null, "OptionSet failed to get 'popup' node.") + input.placeholder_text = placeholder + input.connect("focus_entered", input, "set", ["caret_position", input.max_length]) + input.connect("focus_exited", input, "deselect") + + +func show_options(options_array: Array): + options.clear_items() + options.add_items(options_array) + options.select(options_array.find(input.text)) + options.connect("selection_changed", self, "popup_result") + popup.open_popup(input.placeholder_text, options) + + +func popup_result(index: int, text: String): + if index != -1: + selected_idx = index + input.text = text + input.caret_position = input.max_length + popup.close_popup() + + diff --git a/ui/option_set/option_set.tscn b/ui/option_set/option_set.tscn new file mode 100644 index 0000000..b971429 --- /dev/null +++ b/ui/option_set/option_set.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/option_set/option_set.gd" type="Script" id=1] +[ext_resource path="res://fonts/font_icons.tres" type="DynamicFont" id=2] + +[node name="option_set" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) + +[node name="input" type="LineEdit" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_right = -100.0 +size_flags_horizontal = 3 +max_length = 4096 +placeholder_text = "placeholder" +caret_blink = true +caret_blink_speed = 0.5 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="button" type="Button" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = -100.0 +grow_horizontal = 0 +custom_fonts/font = ExtResource( 2 ) +text = "" +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/ui/option_set/option_set_list.gd b/ui/option_set/option_set_list.gd new file mode 100644 index 0000000..fda2c04 --- /dev/null +++ b/ui/option_set/option_set_list.gd @@ -0,0 +1,294 @@ +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(): + var button_style := get_stylebox("pressed", "Button") + var label_style := get_stylebox("normal", "Label") + + 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 + normal_style.content_margin_left = label_style.content_margin_left + normal_style.content_margin_top = label_style.content_margin_top + normal_style.content_margin_right = label_style.content_margin_right + + var bg_color := button_style.bg_color as Color + var corner_radius := button_style.corner_radius_top_right as int + selected_style = StyleBoxFlat.new() + selected_style.bg_color = bg_color + selected_style.corner_radius_top_left = corner_radius + selected_style.corner_radius_top_right = corner_radius + selected_style.corner_radius_bottom_right = corner_radius + selected_style.corner_radius_bottom_left = corner_radius + selected_style.content_margin_left = label_style.content_margin_left + selected_style.content_margin_top = label_style.content_margin_top + selected_style.content_margin_right = label_style.content_margin_right + + +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_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 + 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) + + diff --git a/ui/option_set/option_set_list.tscn b/ui/option_set/option_set_list.tscn new file mode 100644 index 0000000..82473a5 --- /dev/null +++ b/ui/option_set/option_set_list.tscn @@ -0,0 +1,9 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://ui/option_set/option_set_list.gd" type="Script" id=1] + +[node name="option_set_list" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +rect_clip_content = true +script = ExtResource( 1 ) diff --git a/ui/pointer_input_sensor.gd b/ui/pointer_input_sensor.gd new file mode 100644 index 0000000..f99fc17 --- /dev/null +++ b/ui/pointer_input_sensor.gd @@ -0,0 +1,117 @@ +extends Control +class_name PointerInputSensor + +# All on_ACTION signals have a single argument of type PointerInputData. +signal on_press +signal on_release +#signal on_release_outside +signal on_click +signal on_enter +signal on_exit +#signal on_exit_app_window +signal on_begin_drag +signal on_drag +signal on_end_drag +signal on_scroll + +enum PointerInputAction { + UNDEFINED, + ON_PRESS, + ON_RELEASE, +# ON_RELEASE_OUTSIDE, + ON_CLICK, + ON_ENTER, + ON_EXIT, +# ON_EXIT_APP_WINDOW, + ON_BEGIN_DRAG, + ON_DRAG, + ON_END_DRAG, + ON_SCROLL, +} + +class PointerInputData: + var target: PointerInputSensor + var event: InputEvent + var index := -1 + var initial_position := Vector2.ZERO + var current_position := Vector2.ZERO + var relative_position := Vector2.ZERO + var velocity := Vector2.ZERO + var was_dragged := false + var is_pressed := false + var scroll := 0.0 + var action: int = PointerInputAction.UNDEFINED + + +export var drag_threshold_cm: float = 0.250 + +var pointer: PointerInputData + +onready var screen_dpcm := float(OS.get_screen_dpi()) / 2.54 + + +func _ready(): + pointer = PointerInputData.new() + pointer.target = self + connect("mouse_entered", self, "_on_enter_exit", [true]) + connect("mouse_exited", self, "_on_enter_exit", [false]) + + +func _on_enter_exit(is_inside: bool): + pointer.action = PointerInputAction.ON_ENTER if is_inside else PointerInputAction.ON_EXIT + emit_signal("on_enter" if is_inside else "on_exit", pointer) + + +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 + + pointer.event = event + pointer.action = PointerInputAction.UNDEFINED + pointer.velocity = Vector2.ZERO + pointer.relative_position = Vector2.ZERO + pointer.scroll = 0.0 + pointer.current_position = get_global_mouse_position() + + if event is InputEventScreenTouch && (pointer.is_pressed == false || pointer.index == event.index): + var touch := event as InputEventScreenTouch + pointer.is_pressed = event.pressed + + if pointer.is_pressed: + pointer.index = touch.index + pointer.initial_position = pointer.current_position + pointer.action = PointerInputAction.ON_PRESS + emit_signal("on_press", pointer) + else: + pointer.action = PointerInputAction.ON_RELEASE + emit_signal("on_release", pointer) + if pointer.was_dragged == false: + pointer.action = PointerInputAction.ON_CLICK + emit_signal("on_click", pointer) + else: + pointer.action = PointerInputAction.ON_END_DRAG + emit_signal("on_end_drag", pointer) + pointer.index = -1 + pointer.was_dragged = false + + if event is InputEventScreenDrag && event.index == pointer.index: + var drag := event as InputEventScreenDrag + pointer.velocity = drag.speed + pointer.relative_position = drag.relative + if pointer.was_dragged == false && pointer.current_position.distance_to(pointer.initial_position) > drag_threshold_cm * screen_dpcm: + pointer.was_dragged = true + pointer.action = PointerInputAction.ON_BEGIN_DRAG + emit_signal("on_begin_drag", pointer) + pointer.action = PointerInputAction.ON_DRAG + emit_signal("on_drag", pointer) + + if event is InputEventMouseButton && event.is_pressed(): + if event.button_index == BUTTON_WHEEL_UP || event.button_index == BUTTON_WHEEL_DOWN: + pointer.scroll = -1.0 if event.button_index == BUTTON_WHEEL_UP else 1.0 + pointer.action = PointerInputAction.ON_SCROLL + emit_signal("on_scroll", pointer) + + diff --git a/ui/touch_item_list/touch_item_list.gd b/ui/touch_item_list/touch_item_list.gd new file mode 100644 index 0000000..6794138 --- /dev/null +++ b/ui/touch_item_list/touch_item_list.gd @@ -0,0 +1,68 @@ +extends ItemList +class_name TouchItemList + +const POINTER_VELOCITY_DECAYING_FACTOR: float = PI +const POINTER_VELOCITY_BOOST_FACTOR: float = 1.25 +const EXACT_SELECTION: bool = true + +var is_pointer_dragging := false +var pointer_drag_velocity := 0.0 +var when_last_dragged := 0 + +onready var sensor := get_node("sensor") as PointerInputSensor +onready var v_scroll_bar := get_v_scroll() as ScrollBar + + +func _ready(): + 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 _process(delta: float): + # 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 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 selected_idx := get_item_at_position(pointer.current_position - rect_global_position, EXACT_SELECTION) + if selected_idx >= 0: + select(selected_idx) + emit_signal("item_selected", selected_idx) + else: + unselect_all() + emit_signal("nothing_selected") + + +func pointer_input_on_scroll_handler(pointer: PointerInputSensor.PointerInputData): + var target := self + target._gui_input(pointer.event) + + diff --git a/ui/touch_item_list/touch_item_list.tscn b/ui/touch_item_list/touch_item_list.tscn new file mode 100644 index 0000000..38f9a7e --- /dev/null +++ b/ui/touch_item_list/touch_item_list.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/pointer_input_sensor.gd" type="Script" id=1] +[ext_resource path="res://ui/touch_item_list/touch_item_list.gd" type="Script" id=2] + +[node name="item_list" type="ItemList"] +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +script = ExtResource( 2 ) + +[node name="sensor" type="Control" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_right = -8.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/ui/touch_vertical_container/touch_vertical_container.gd b/ui/touch_vertical_container/touch_vertical_container.gd new file mode 100644 index 0000000..8f87338 --- /dev/null +++ b/ui/touch_vertical_container/touch_vertical_container.gd @@ -0,0 +1,118 @@ +extends ScrollContainer +class_name TouchVerticalContainer + +const POINTER_VELOCITY_DECAYING_FACTOR: float = PI +const POINTER_VELOCITY_BOOST_FACTOR: float = 1.25 + +var is_pointer_dragging := false +var pointer_drag_velocity := 0.0 +var when_last_dragged := 0 +var exclude_controls := [] + +onready var controls = get_node("controls") + + +func _ready(): + assert(controls != null, "TouchVerticalContainer failed to get 'controls' node.") + + var item_list_separation := float(controls.get_constant("separation")) + + for it in controls.get_children(): + it = it as Control + if exclude_controls.has(it.name): + continue + + var sensor = PointerInputSensor.new() + it.add_child(sensor) + + sensor.name = "sensor" + sensor.anchor_right = 1.0 + sensor.anchor_bottom = 1.0 + sensor.margin_top = - item_list_separation / 2.0 + sensor.margin_bottom = item_list_separation / 2.0 + + 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 _process(delta: float): + # 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) + self.scroll_vertical -= pointer_drag_velocity * delta + + +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 + self.scroll_vertical -= 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): + # Get last leaf node. + var root := pointer.target.get_parent() as Control + var leaf := root as Node + while leaf.get_child_count() > 0: + leaf = leaf.get_child(leaf.get_child_count() - 1) + + # Navigate backwards from leaf to root until we find a node accepting the input. + var tried_leaf_as_root := false + while leaf != root || tried_leaf_as_root == false: + tried_leaf_as_root = leaf == root # Allow a final iteration cycle when leaf reaches root. + var node := leaf + + if node is PointerInputSensor \ + || node is Control == false \ + || node.mouse_filter == MOUSE_FILTER_IGNORE \ + || node.visible == false \ + || node.get_global_rect().has_point(pointer.current_position) == false: + # Get next node to be processed. + var leaf_index_in_parent := leaf.get_position_in_parent() + var parent := leaf.get_parent() + if leaf_index_in_parent == 0: + leaf = parent + else: + leaf = parent.get_child(leaf_index_in_parent - 1) + # Drill down into the new tree branch. + while leaf.get_child_count() > 0: + leaf = leaf.get_child(leaf.get_child_count() - 1) + continue + + var control: Control = node + if control is CheckBox || control is CheckButton || (control is Button && control.toggle_mode == true): + control.pressed = !control.pressed + + control.grab_focus() + control.emit_signal("button_down") + control.emit_signal("pressed") + control.emit_signal("button_up") + control.connect("focus_exited", pointer.target, "set_visible", [true], CONNECT_ONESHOT) + pointer.target.visible = false + break + + +func pointer_input_on_scroll_handler(pointer: PointerInputSensor.PointerInputData): + var target := self + target._gui_input(pointer.event) + + diff --git a/ui/touch_vertical_container/touch_vertical_container.tscn b/ui/touch_vertical_container/touch_vertical_container.tscn new file mode 100644 index 0000000..e741e8a --- /dev/null +++ b/ui/touch_vertical_container/touch_vertical_container.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://ui/touch_vertical_container/touch_vertical_container.gd" type="Script" id=1] + +[node name="touch_vertical_container" type="ScrollContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +scroll_horizontal_enabled = false +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="controls" type="VBoxContainer" parent="."] +margin_right = 1080.0 +margin_bottom = 1920.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 |
