Files
imi_skysens/table/table.gd

607 lines
23 KiB
GDScript
Raw Permalink 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 node_gap: = Vector2(0.0, 0.0) ## Зазор между ячейками, который не возможно вычислить или получить
@export var selector_color: = Color(1.0, 1.0, 1.0, 1.0) ## Окрашивающий цвет для индикатора выбранного ряда
@export var selector_key_down: Key = KEY_DOWN ## Клавиша увеличения индекса выбранного ряда
@export var selector_key_up: Key = KEY_UP ## Клавиша уменьшения индекса выбранного ряда
@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 ## Сцена индикатора для выбранного ряда
const HEAD_PREFIX = 'HEAD_' ## Префикс ключа заголовка таблицы
const INDEX_FORMAT = '%03d_%03d' ## Формат индекса ключа
const MAX_INDEX = 200 ## Максимальное значение индекса для колонки и строки. Связано с форматом индекса ключа
const NODE_PREFIX = 'NODE_' ## Префикс ключа ячейки таблицы
const SLCT_PREFIX = 'SLCT_' ## Префикс ключа индикатора выбранного ряда таблицы
const HEAD_KEY_FORMAT = HEAD_PREFIX + INDEX_FORMAT ## Формат ключа заголовка
const NODE_KEY_FORMAT = NODE_PREFIX + INDEX_FORMAT ## Формат ключа ячейки
const ROW_SELECTOR_KEY_FORMAT = SLCT_PREFIX + INDEX_FORMAT ## Формат ключа индикаторов выбранного ряда
const META_USER_DATA = 'user_data' ## Имя свойства для сохранения данных пользователя
const META_CALLABLE = 'callable' ## Имя свойства для сохранения вызываемого
const META_EVENT_NAME = 'event_name' ## Имя свойства для сохранения названия события
var header: GridContainer ## Заголовок таблицы
var in_focus: = false ## Фокус пользовательского ввода
var nodes: = {} ## Все ячейки таблицы и дополнительные элементы
var ScnNode: PackedScene ## Прототип ячейки таблицы
var ScnSelector: PackedScene ## Прототип индикатора выбранного ряда
var selector_color_proc: Callable ## Ссылка на процедуру генератора цвета индикатора выбранного ряда
var selector_size_proc: Callable ## Ссылка на процедуру генератора размера индикатора выбранного ряда
signal row_pressed(i_row: int) ## Вызывается при нажатии кнопкой мыши на строку
signal selected_changed(i_row: int) ## Вызывается при изменении выбранного ряда. i_row - текущий выбранный ряд
var scroll: Control
var scroll_adj: = Vector2(0, 0)
func get_node_scene(): return node_type_file
func _assert_meta(i_col: int, i_row: int, pname: String, node): assert(node.has_meta(pname), 'ошибка: нет мета-свойства \"%s\" у ячейки колонка:%d ряд:%d' % [pname, i_col, i_row])
func get_node_user_data(i_col: int, i_row: int): return get_node_meta(i_col, i_row, META_USER_DATA)
func set_node_user_data(i_col: int, i_row: int, val) -> void: set_node_meta(i_col, i_row, META_USER_DATA, val)
## Закрепляет заголовок таблицы. [param scrl] - Прокрутка,
## [param prnt] - Родитель прокрутки заданный в редакторе
func pin_header(scrl: ScrollContainer, prnt: Control):
scrl.remove_child(header)
prnt.add_child(header)
## TODO: Это значение следует получить из темы
var v_separation = get_theme_constant('v_separation')
header.position = scrl.position - Vector2(0, header.size.y + v_separation)
scroll = scrl
scroll_adj = scrl.position
## Перемещает указатель на одну строку вверх
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)
var node = get_node2(0, i_row)
if scroll is ScrollContainer:
scroll.ensure_control_visible(node)
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)
var node = get_node2(0, i_row)
if scroll is ScrollContainer:
scroll.ensure_control_visible(node)
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 _ready():
ScnSelector = load(selector_type_file)
ScnNode = load(node_type_file)
assert(ScnSelector)
assert(ScnNode)
## Выравнивает по нижнему левому углу. Используется для выравнивания по заголовку
func align_to_left_bottompos(hdr: Control, gap: = Vector2(0, 0)):
position.x = hdr.position.x + gap.x
position.y = hdr.position.y + hdr.size.y + gap.y
## [param i_col] - индекс колонки. [param 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)
## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
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)
## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
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)
## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
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)
## [param i_col] - индекс колонки. [param i_row] - индекс ряда
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 event.pressed and in_focus:
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 check_event_pos(event.position, scroll):
if not in_focus:
in_focus = true
emit_signal('focus_entered')
elif not check_event_pos(event.position, scroll):
if in_focus:
in_focus = false
emit_signal('focus_exited')
##
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())
scroll = self
##
func check_event_pos(event_pos: Vector2, obj: Control):
return \
(event_pos.x > obj.position.x) \
and (event_pos.x < (obj.position.x + obj.size.x)) \
and (event_pos.y > obj.position.y) \
and (event_pos.y < (obj.position.y + obj.size.y))
##
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())
## [param i_row] - Индекс ряда
func default_selector_size_proc(i_row: int, rect: Array) -> void:
get_row_rect(i_row, rect)
rect[0] += position.x - node_gap.x / 2.0
rect[1] += position.y - node_gap.y / 2.0
rect[2] += node_gap.x
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)
## [param i_row] - Индекс ряда.
func set_row_selected(i_row: int, selected: bool = true) -> void:
var key: = ROW_SELECTOR_KEY_FORMAT % [i_row, 0]
var node = nodes[NODE_KEY_FORMAT % [i_row, 0]]
if nodes.has(key) and (not selected):
var selector = nodes[key]
node.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.size.x = rect[2]
selector.size.y = rect[3]
selector.modulate = selector_color_proc.call(i_row)
selector.z_index = z_index + 1
nodes[key] = selector
node.add_child(selector)
## [param i_row] - Индекс ряда.
func is_row_selected(i_row: int) -> bool:
var key: = ROW_SELECTOR_KEY_FORMAT % [i_row, 0]
return nodes.has(key)
## [param i_row] - Индексы рядов.
func get_selected_rows(i_rows: Array) -> void:
for key in nodes.keys():
if SLCT_PREFIX in key:
var indexes = get_key_indexes(key)
i_rows.append(indexes[1])
## Возвращает количество рядов
func get_rows_count() -> int:
var nodes_count: int = 0
for key in nodes:
nodes_count += int(NODE_PREFIX in key)
return nodes_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 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 i_row: int = get_rows_count()
assert(i_row < MAX_INDEX, 'ошибка: слишком много рядов, больше %d' % (MAX_INDEX + 1))
for i_col in columns:
var node_type = row_types[i_col]
var node = node_type.instantiate()
assert(node.name == node.name, 'ошибка: экземпляр не имеет поля \"name\"')
var head = nodes[HEAD_KEY_FORMAT % [0, i_col]]
node.custom_minimum_size.x = head.custom_minimum_size.x
node.set_deferred('size.x', head.size.x)
var node_key: String = NODE_KEY_FORMAT % [i_row, i_col]
node.name = node_key
add_child(node)
nodes[node_key] = node
var conn = Callable(self, '_on_focus_entered').bind(node)
node.connect('focus_entered', conn)
set_node_meta(i_col, i_row, 'callable_focus_entered', conn)
##
func _on_focus_entered(node):
var indexes = get_key_indexes(node.name)
emit_signal('row_pressed', indexes[1])
##
func set_header(header_types: Array):
assert(header_types.size() < MAX_INDEX, 'ошибка: количество колонок в заголовке: %d больше допустимого: %d' % [header_types.size(), MAX_INDEX + 1])
if columns == 1:
set_columns(header_types.size())
else:
assert(columns == header_types.size(), 'ошибка: количество колонок в заголовке %d, но ранее установлено: %d' % [header_types.size(), columns])
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 = scroll.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 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]
var prnt = selector.get_parent()
prnt.remove_child(selector)
selector.queue_free()
nodes.erase(sel_key)
for i_col in columns:
var node_key: String = NODE_KEY_FORMAT % [i_row, i_col]
var node = nodes[node_key]
var conn = get_node_meta(i_col, i_row, 'callable_focus_entered')
node.disconnect('focus_entered', conn)
if node.has_meta(META_CALLABLE):
conn = node.get_meta(META_CALLABLE)
var event_name = node.get_meta(META_EVENT_NAME)
node.disconnect(event_name, conn)
remove_child(node)
nodes.erase(node_key)
node.queue_free()
rehash()
##
func next_index(indexes: Array) -> void:
indexes[0].x += 1
if indexes[0].x == columns:
indexes[0].x = 0
indexes[0].y += 1
##
func swap_nodes(nodes_copy, node_key_format, i0, i1) -> void:
var key0 = node_key_format % [i0.y, i0.x]
var key1 = node_key_format % [i1.y, i1.x]
nodes_copy[key1] = nodes[key0]
nodes_copy[key1].name = key1
## Переназначает ключи в соответствии с размерами таблицы.
func rehash():
var i_node: = [Vector2i()]
var i_hdr: = [Vector2i()]
var i_slct: = [Vector2i()]
var nodes_copy = {}
for key in nodes:
var iv = get_key_indexes(key)
var i0 = Vector2(iv[0], iv[1])
if HEAD_PREFIX in key:
swap_nodes(nodes_copy, HEAD_KEY_FORMAT, i0, i_hdr[0])
next_index(i_hdr)
elif NODE_PREFIX in key:
swap_nodes(nodes_copy, NODE_KEY_FORMAT, i0, i_node[0])
next_index(i_node)
elif SLCT_PREFIX in key:
swap_nodes(nodes_copy, ROW_SELECTOR_KEY_FORMAT, i0, i_slct[0])
next_index(i_slct)
nodes.clear()
nodes.merge(nodes_copy)
## Удаляет все строки таблицы
func clear_rows() -> void:
var rows_count: int = get_rows_count()
for i_row in range(rows_count - 1, -1, -1):
remove_row(i_row)
## Настраевает обратные вызовы для колонок.
## [param dest_node] - Получатель события[br]
## [param event_name] - Имя события[br]
## [param callback] - Имя функции получателя события[br]
## [param clmns] - Индексы колонок[br]
## [param user_data] - Аргументы функции получателя события[br]
func connect_columns(dest_node: Node, event_name: String, callback: String, clmns: Array, user_data: Array) -> void:
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 = get_node2(i_col, i_row)
if node.has_meta(META_CALLABLE):
var conn = node.get_meta(META_CALLABLE)
node.disconnect(event_name, conn)
var conn = Callable(dest_node, callback).bind([node] + user_data)
if OK == node.connect(event_name, conn):
node.set_meta(META_CALLABLE, conn)
node.set_meta(META_EVENT_NAME, event_name)
else:
push_error('не удаётся подключить событие \"%s\" %s к %s', event_name, conn, 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.set_deferred('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
## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
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
## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
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
## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
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
## [param i_row] - индекс ряда.
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]
## [param i_row] - индекс ряда.
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 >= 0)
assert((columns - i_begin) <= row.size())
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
## Возвращает массив ячеек из ряда [param i_row]
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
## Возвращает прямоугольник [param rect] занимаемый ячейкой в ряду [param i_row] и колонке [param i_col].
## [param key_format] - Формат ключа ячейки
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
## Возвращает прямоугольник [param rect] занимаемый рядом [param i_row]. [param key_format] - Формат ключа ячейки
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 + scroll_adj.x
rect[1] = cell0.position.y + scroll_adj.y
rect[2] = (cell1.position.x - cell0.position.x) + cell1.size.x
rect[3] = cell0.size.y