Files
uarep-ctl/table/table.gd
2023-07-12 08:22:05 +03:00

540 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends GridContainer
@export var selector_color: = Color(1.0, 1.0, 1.0, 1.0)
@export var node_gap: = Vector2(0.0, 0.0) #
@export_file var node_type_file: = 'res://table/node.tscn': get = get_node_scene, set = load_node_scene
@export_file var selector_type_file: = 'res://table/selector.tscn': set = load_selector_scene
@export var selector_key_up: Key = KEY_UP # Клавиша уменьшения индекса выбранного ряда
@export var selector_key_down: Key = KEY_DOWN # Клавиша увеличения индекса выбранного ряда
const MAX_INDEX = 99 # Максимальное значение индекса для колонки и строки. Связано с форматом индекса ключа
const INDEX_FORMAT = '%02d_%02d' # Формат индекса ключа
const NODE_PREFIX = 'NODE_' # Префикс ключа ячейки таблицы
const HEAD_PREFIX = 'HEAD_' # Префикс ключа заголовка таблицы
const DATA_PREFIX = 'DATA_' # Префикс ключа данных ячейки таблицы
const CONN_PREFIX = 'CONN_' # Префикс ключа обработчиков событий ячейки таблицы
const SLCT_PREFIX = 'SLCT_' # Префикс ключа индикатора выбранного ряда таблицы
const NODE_KEY_FORMAT = NODE_PREFIX + INDEX_FORMAT
const HEAD_KEY_FORMAT = HEAD_PREFIX + INDEX_FORMAT
const DATA_KEY_FORMAT = DATA_PREFIX + INDEX_FORMAT
const CONNECTED_KEY_FORMAT = CONN_PREFIX + INDEX_FORMAT
const ROW_SELECTOR_KEY_FORMAT = SLCT_PREFIX + INDEX_FORMAT
var nodes: = {} # Все ячейки таблицы и дополнительные элементы
var selector_color_proc: Callable # Ссылка на процедуру генератора цвета индикатора выбранного ряда
var selector_size_proc: Callable # Ссылка на процедуру генератора размера индикатора выбранного ряда
var ScnSelector: PackedScene # Прототип индикатора выбранного ряда
var ScnNode: PackedScene # Прототип ячейки таблицы
var in_focus: = false # Фокус пользовательского ввода
var header: GridContainer # Заголовок таблицы
signal row_pressed(i_row: int) # Вызывается при нажатии кнопкой мыши на строку
signal selected_changed(i_row: int) # Вызывается при изменении выбранного ряда. i_row - текущий выбранный ряд
func selector_up():
var i_rows: = []
get_selected_rows(i_rows)
for i in i_rows:
set_row_selected(i, false)
var rows_count: = get_rows_count()
if not (i_rows.size() and (rows_count > 1)):
return
var i_row = i_rows[0]
i_row -= 1
if i_row < 0:
i_row = rows_count - 1
set_row_selected(i_row, true)
emit_signal('selected_changed', i_row)
func selector_down():
var i_rows: = []
get_selected_rows(i_rows)
for i in i_rows:
set_row_selected(i, false)
var rows_count: = get_rows_count()
if not (i_rows.size() and (rows_count > 1)):
return
var i_row = i_rows[0]
i_row += 1
i_row %= rows_count
set_row_selected(i_row, true)
emit_signal('selected_changed', i_row)
func set_columns_alignments(alignments):
assert(columns == alignments.size())
var rows_count: = get_rows_count()
for i_row in rows_count:
for i_col in alignments.size():
set_node_alligment(i_col, i_row, alignments[i_col])
func load_selector_scene(file_name: String):
selector_type_file = file_name
ScnSelector = load(file_name)
func load_node_scene(file_name: String):
node_type_file = file_name
ScnNode = load(file_name)
func get_node_scene():
return node_type_file
func _ready():
ScnSelector = load(selector_type_file)
ScnNode = load(node_type_file)
assert(ScnSelector)
assert(ScnNode)
func align_to_left_bottom(header, gap: = Vector2(0, 0)):
""" Выравнивает по нижнему левому углу.
Используется для выравнивания по заголовку """
position.x = header.position.x + gap.x
position.y = header.position.y + header.size.y + gap.y
func _assert_meta(i_col: int, i_row: int, pname: String, node):
assert(node.has_meta(pname), 'ошибка: нет мета-свойства \"%s\" у ячейки (%s): колонка:%d ряд:%d' % [pname, typeof(node), i_col, i_row])
func set_node_meta(i_col: int, i_row: int, pname: String, val) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
var node = nodes[key]
node.set_meta(pname, val)
func get_node_meta(i_col: int, i_row: int, pname: String):
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
var node = nodes[key]
_assert_meta(i_col, i_row, pname, node)
return node.get_meta(pname)
func set_node_property(i_col: int, i_row: int, pname: String, val) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
var node = nodes[key]
node.set(pname, val)
func get_node_property(i_col: int, i_row: int, pname: String):
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
var node = nodes[key]
return node.get(pname)
func get_node_key(format: String, i_col: int, i_row: int) -> String:
var key: String = format % [i_row, i_col]
assert(nodes.has(key), 'ошибка: нет такой ячейки: колонка:%d ряд:%d' % [i_col, i_row])
return key
func get_key_indexes(key: String) -> Array:
var tokens: = key.split('_')
assert(tokens.size() == 3, 'ошибка: неверный ключ: \"%s\"' % key)
return [int(tokens[2]), int(tokens[1])]
func set_columns_min_size(sizes: Array) -> void:
var rows_count: = get_rows_count()
for i_row in rows_count:
set_nodes_min_size(i_row, sizes)
set_nodes_min_size(0, sizes, HEAD_KEY_FORMAT)
func set_columns_aligment(alignments: Array) -> void:
var rows_count: = get_rows_count()
for i_row in rows_count:
for i_col in alignments.size():
set_node_alligment(i_col, i_row, alignments[i_col])
func _input(event) -> void:
if event is InputEventKey:
if in_focus and event.pressed:
if event.keycode == selector_key_up:
selector_up()
elif event.keycode == selector_key_down:
selector_down()
elif event is InputEventMouseButton:
if not event.pressed:
return
if (event.position.x < position.x) \
or (event.position.x > (position.x + size.x)) \
or (event.position.y < position.y) \
or (event.position.y > (position.y + size.y)):
in_focus = false
emit_signal('focus_exited')
return
var rows_count: = get_rows_count()
in_focus = true
emit_signal('focus_entered')
for i_row in rows_count:
var rect = [0, 0, 0, 0]
get_node_rect(0, i_row, rect)
var y = rect[1] + position.y
if event.position.y < y:
continue
if event.position.y > (y + rect[3]):
continue
emit_signal('row_pressed', i_row)
break
func _init() -> void:
selector_color_proc = Callable(self, 'defaultselector_color_proc')
assert(selector_color_proc.is_valid())
selector_size_proc = Callable(self, 'default_selector_size_proc')
assert(selector_size_proc.is_valid())
func defaultselector_color_proc(_i_row: int):
return selector_color
func setselector_color_proc(instance: Object, function: String) -> void:
selector_color_proc = Callable(instance, function)
assert(selector_color_proc.is_valid())
func set_selector_size_proc(instance: Object, function: String) -> void:
selector_size_proc = Callable(instance, function)
assert(selector_size_proc.is_valid())
func default_selector_size_proc(i_row: int, rect: Array) -> void:
get_row_rect(i_row, rect)
rect[0] = rect[0] + position.x - node_gap.x / 2.0
rect[1] = rect[1] + position.y - node_gap.y / 2.0
rect[2] = rect[2] + node_gap.x
rect[3] = rect[3] + node_gap.y
func clear_rows_selected():
var rows_count: = get_rows_count()
for i_row in rows_count:
set_row_selected(i_row, false)
func set_row_selected(i_row: int, selected: bool = true) -> void:
var key: = ROW_SELECTOR_KEY_FORMAT % [i_row, 0]
var parent = get_parent()
if nodes.has(key) and (not selected):
var selector = nodes[key]
parent.remove_child(selector)
selector.queue_free()
nodes.erase(key)
elif (not nodes.has(key)) and selected:
var selector = ScnSelector.instantiate()
var rect: = [0, 0, 0, 0]
selector_size_proc.call(i_row, rect)
selector.position.x = rect[0]
selector.position.y = rect[1]
selector.size.x = rect[2]
selector.size.y = rect[3]
selector.modulate = selector_color_proc.call(i_row)
nodes[key] = selector
parent.add_child(selector)
func is_row_selected(i_row: int) -> bool:
var key: = get_node_key(ROW_SELECTOR_KEY_FORMAT, 0, i_row)
return nodes.has(key)
func get_selected_rows(rows: Array) -> void:
for key in nodes.keys():
if SLCT_PREFIX in key:
var indexes = get_key_indexes(key)
rows.append(indexes[1])
func get_rows_count() -> int:
var rows_count: int = 0
for key in nodes:
rows_count += int(NODE_PREFIX in key)
return rows_count / columns
func get_node2(i_col: int, i_row: int) -> Node:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
return nodes[key]
func set_node_user_data(i_col: int, i_row: int, user_data: Object) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
nodes[key] = user_data
func add_row(row_types: Array) -> void:
assert(row_types.size() < MAX_INDEX, 'ошибка: слишком много колонок, больше %d' % (MAX_INDEX + 1))
if columns == 1:
set_columns(row_types.size())
else:
assert(columns == row_types.size(), 'ошибка: %s количество колонок установлено ранее - %d, но требуется %d' % [self, columns, row_types.size()])
var rows_count: int = get_rows_count()
assert(rows_count < MAX_INDEX, 'ошибка: слишком много рядов, больше %d' % (MAX_INDEX + 1))
for i_col in columns:
var node = row_types[i_col].instantiate()
var key: String = NODE_KEY_FORMAT % [rows_count, i_col]
node.name = key
add_child(node)
nodes[key] = node
func set_header(header_types: Array):
assert(header_types.size() < MAX_INDEX, 'ошибка: слишком много колонок, больше %d' % (MAX_INDEX + 1))
if columns == 1:
set_columns(header_types.size())
else:
assert(columns == header_types.size())
var do_adj: = false
if not header:
header = GridContainer.new()
header.position = position
header.columns = header_types.size()
header.size.x = size.x
var parent = get_parent()
parent.add_child(header)
do_adj = true
else:
var childrens = header.get_children()
for node in childrens:
childrens.erase(node.name)
childrens.remove_child(node)
node.queue_free()
for i_col in columns:
var node = header_types[i_col].instantiate()
var key: String = HEAD_KEY_FORMAT % [0, i_col]
node.name = key
nodes[key] = node
header.add_child(node)
if do_adj:
var rect = [0, 0, 0, 0]
get_node_rect(0, 0, rect, HEAD_KEY_FORMAT)
position.y += rect[3]
func set_header_text(headers: Array):
assert(columns == headers.size())
set_row_text(0, headers, 0, HEAD_KEY_FORMAT)
func get_node_user_data(i_col: int, i_row: int) -> Object:
var key: = get_node_key(DATA_KEY_FORMAT, i_col, i_row)
return nodes[key]
func remove_row(i_row: int):
var sel_key: String = ROW_SELECTOR_KEY_FORMAT % [i_row, 0]
if nodes.has(sel_key):
var selector = nodes[sel_key]
get_parent().remove_child(selector)
selector.queue_free()
nodes.erase(sel_key)
for i_col in columns:
var node_key: String = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
var conn_key: String = CONNECTED_KEY_FORMAT % [i_row, i_col]
var data_key: String = DATA_KEY_FORMAT % [i_row, i_col]
var node = nodes[node_key]
nodes.erase(node_key)
remove_child(node)
node.queue_free()
if nodes.has(data_key):
nodes.erase(data_key)
if nodes.has(conn_key):
var conn = nodes[conn_key]
var callback = conn[0]
var dest_node = conn[1]
node.disconnect('pressed', Callable(dest_node, callback))
nodes.erase(conn_key)
func rehash(swaps: Array):
""" Переназначает ключи в соответствии с размерами таблицы.
После вызова swaps содержит массив пар [<прежний ключ>, <текущий ключ>] """
assert(not swaps.size(), 'ошибка: swaps не пустой')
var nodes_copy: = {}
var i_col: = 0
var i_row: = 0
for key0 in nodes:
if NODE_PREFIX in key0:
var node_key1: String = NODE_KEY_FORMAT % [i_row, i_col]
nodes_copy[node_key1] = nodes[key0]
swaps.append([key0, node_key1])
var indxs = get_key_indexes(key0)
var slct_key0: String = ROW_SELECTOR_KEY_FORMAT % [indxs[1], indxs[0]]
if nodes.has(slct_key0):
var slct_key1: String = ROW_SELECTOR_KEY_FORMAT % [i_row, i_col]
nodes_copy[slct_key1] = nodes[slct_key0]
swaps.append([slct_key0, slct_key1])
var conn_key0: String = CONNECTED_KEY_FORMAT % [indxs[1], indxs[0]]
if nodes.has(conn_key0):
var conn_key1: String = CONNECTED_KEY_FORMAT % [i_row, i_col]
nodes_copy[conn_key1] = nodes[conn_key0]
swaps.append([conn_key0, conn_key1])
var data_key0: String = DATA_KEY_FORMAT % [indxs[1], indxs[0]]
if nodes.has(data_key0):
var data_key1: String = DATA_KEY_FORMAT % [i_row, i_col]
nodes_copy[data_key1] = nodes[data_key0]
swaps.append([data_key0, data_key1])
i_col += 1
if i_col == columns:
i_col = 0
i_row += 1
nodes.clear()
nodes.merge(nodes_copy)
func clear_rows() -> void:
""" Удаляет все строки таблицы """
var rows_count: int = get_rows_count()
for i_row in rows_count:
remove_row(i_row)
func set_callbacks(dest_node: Node, event_name: String, callback: String, clmns: Array, user_data: Array) -> void:
""" Настраивает обратные вызовы для колонок.
:dest_node: Получатель события
:event_name: Имя события
:callback: Имя функции получателя события
:clmns: Индексы колонок
:user_data: Аргументы функции получателя события """
var rows_count = get_rows_count()
for i_row in rows_count:
for i_col in clmns:
assert(i_col >= 0 and i_col < columns, 'ошибка: нет колонки %d' % i_col)
var node_key: String = NODE_KEY_FORMAT % [i_row, i_col]
var conn_key: String = CONNECTED_KEY_FORMAT % [i_row, i_col]
var node = nodes[node_key]
if nodes.has(conn_key):
var conn = nodes[conn_key]
var prev_callback = conn[0]
var prev_dest_node = conn[1]
node.disconnect(event_name, Callable(prev_dest_node, prev_callback))
assert (OK == node.connect(event_name, Callable(dest_node, callback).bind([node] + user_data)))
nodes[conn_key] = [callback, dest_node]
func set_rows_text(rows: Array) -> void:
for i_row in rows.size():
for i_col in columns:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
var node = nodes[key]
node.text = rows[i_row][i_col]
func set_nodes_min_size(i_row: int, sizes: Array, key_format: String = NODE_KEY_FORMAT) -> void:
for i_col in columns:
var key: = get_node_key(key_format, i_col, i_row)
var node = nodes[key]
node.custom_minimum_size.x = sizes[i_col]
node.size.x = sizes[i_col]
func set_node_text(i_col: int, i_row: int, val: String) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
nodes[key].text = val
func set_rows_state(rows: Array) -> void:
for i_row in rows.size():
for i_col in columns:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
nodes[key].button_pressed = rows[i_row][i_col]
func set_node_state(i_col: int, i_row: int, val: bool) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
nodes[key].button_pressed = val
func set_node_frame(i_col: int, i_row: int, node_name: String, val: int) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
var node = nodes[key]
var sprite = node.get_node(node_name)
sprite.frame = val
func set_node_editable(i_col: int, i_row: int, val: bool) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
nodes[key].editable = val
func set_node_alligment(i_col: int, i_row: int, val: int) -> void:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
if 'horizontal_alignment' in nodes[key]:
nodes[key].horizontal_alignment = val
elif 'alignment' in nodes[key]:
nodes[key].alignment = val
func set_row_alligments(i_row: int, aligns: Array) -> void:
for i_col in columns:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
nodes[key].align = aligns[i_col]
func get_row_text(i_row: int) -> Array:
var row: = []
for i_col in columns:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
row.append(nodes[key].text)
return row
func set_row_text(i_row: int, row: Array, i_begin: int = 0, key_format: String = NODE_KEY_FORMAT) -> void:
assert((i_begin + row.size()) < columns)
for i_col in columns - i_begin:
var key: = get_node_key(key_format, i_col + i_begin, i_row)
nodes[key].text = row[i_col]
func get_rows_text() -> Array:
var rows_count: = get_rows_count()
var rows: = []
for i_row in rows_count:
var row = get_row_text(i_row)
rows.append(row)
return rows
func get_row(i_row: int) -> Array:
var row: = []
for i_col in columns:
var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
row.append(nodes[key])
return row
func get_node_rect(i_col: int, i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void:
assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата')
var key: = get_node_key(key_format, i_col, i_row)
var cell = nodes[key]
rect[0] = cell.position.x
rect[1] = cell.position.y
rect[2] = cell.size.x
rect[3] = cell.size.y
func get_row_rect(i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void:
assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата')
var cell0 = nodes[get_node_key(key_format, 0, i_row)]
var cell1 = nodes[get_node_key(key_format, columns - 1, i_row)]
rect[0] = cell0.position.x
rect[1] = cell0.position.y
rect[2] = (cell1.position.x - cell0.position.x) + cell1.size.x
rect[3] = cell0.size.y