@tool extends MarginContainer ## The visual representation of a task. const __Singletons := preload("../../plugin_singleton/singletons.gd") const __Shortcuts := preload("../shortcuts.gd") const __EditContext := preload("../edit_context.gd") const __Filter := preload("../filter.gd") const __BoardData := preload("../../data/board.gd") const __EditLabel := preload("../../edit_label/edit_label.gd") const __ExpandButton := preload("../../expand_button/expand_button.gd") const __TaskData := preload("../../data/task.gd") const __DetailsScript := preload("../details/details.gd") const __StepHolder := preload("../details/step_holder.gd") const __TooltipScript := preload("../tooltip.gd") const __CategoryPopupMenu := preload("../category/category_popup_menu.gd") enum ACTIONS { DETAILS, RENAME, DELETE, DUPLICATE, } const COLOR_WIDTH: int = 8 var board_data: __BoardData: set(value): board_data = value __update_category_menu() var data_uuid: String var __style_focus: StyleBoxFlat var __style_panel: StyleBoxFlat var __category_menu := __CategoryPopupMenu.new() @onready var panel_container: PanelContainer = %Panel @onready var category_button: Button = %CategoryButton @onready var title_label: __EditLabel = %Title @onready var description_label: Label = %Description @onready var step_holder: __StepHolder = %StepHolder @onready var expand_button: __ExpandButton = %ExpandButton @onready var edit_button: Button = %Edit @onready var context_menu: PopupMenu = %ContextMenu @onready var details: __DetailsScript = %Details func _ready() -> void: __style_focus = StyleBoxFlat.new() __style_focus.set_border_width_all(1) __style_focus.draw_center = false __style_panel = StyleBoxFlat.new() __style_panel.set_border_width_all(0) __style_panel.border_width_left = COLOR_WIDTH __style_panel.draw_center = false panel_container.add_theme_stylebox_override(&"panel", __style_panel) context_menu.id_pressed.connect(__action) edit_button.pressed.connect(__action.bind(ACTIONS.DETAILS)) expand_button.state_changed.connect(func (expanded): __update_step_holder()) category_button.pressed.connect(__on_category_button_pressed) add_child(__category_menu) __category_menu.uuid_selected.connect(__on_category_menu_uuid_selected) notification(NOTIFICATION_THEME_CHANGED) await get_tree().create_timer(0.0).timeout var ctx: __EditContext = __Singletons.instance_of(__EditContext, self) update() board_data.get_task(data_uuid).changed.connect(update) board_data.changed.connect(__update_category_button) if data_uuid == ctx.focus: ctx.focus = "" grab_focus() if not ctx.filter_changed.is_connected(__apply_filter): ctx.filter_changed.connect(__apply_filter) ctx.settings.changed.connect(update) __apply_filter() func _gui_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT: accept_event() __update_context_menu() context_menu.position = get_global_mouse_position() if not get_window().gui_embed_subwindows: context_menu.position += get_window().position context_menu.popup() if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed() and event.is_double_click(): __action(ACTIONS.DETAILS) func _shortcut_input(event: InputEvent) -> void: if not __Shortcuts.should_handle_shortcut(self): return var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self) if not event.is_echo() and event.is_pressed(): if shortcuts.delete.matches_event(event): get_viewport().set_input_as_handled() __action(ACTIONS.DELETE) elif shortcuts.confirm.matches_event(event): get_viewport().set_input_as_handled() __action(ACTIONS.DETAILS) elif shortcuts.rename.matches_event(event): get_viewport().set_input_as_handled() __action(ACTIONS.RENAME) elif shortcuts.duplicate.matches_event(event): get_viewport().set_input_as_handled() __action(ACTIONS.DUPLICATE) func _make_custom_tooltip(for_text) -> Object: var tooltip := __TooltipScript.new() tooltip.text = for_text tooltip.mimic_paragraphs() return tooltip func _notification(what: int) -> void: match(what): NOTIFICATION_THEME_CHANGED: if panel_container: var tab_panel = get_theme_stylebox(&"panel", &"TabContainer") if tab_panel is StyleBoxFlat: __style_panel.bg_color = tab_panel.bg_color __style_panel.draw_center = true else: __style_panel.draw_center = false if edit_button: edit_button.icon = get_theme_icon(&"Edit", &"EditorIcons") NOTIFICATION_DRAW: if has_focus(): __style_focus.draw( get_canvas_item(), Rect2( panel_container.get_global_rect().position - get_global_rect().position, panel_container.size ), ) func update() -> void: if not is_inside_tree(): # The node might linger in the undoredo manager. return var ctx: __EditContext = __Singletons.instance_of(__EditContext, self) var task := board_data.get_task(data_uuid) var task_category := board_data.get_category(task.category) __style_focus.border_color = task_category.color __style_panel.border_color = task_category.color category_button.text = task_category.title category_button.visible = ctx.settings.show_category_on_board if ctx.settings.show_description_preview: var description: String match ctx.settings.description_on_board: ctx.settings.DescriptionOnBoard.FIRST_LINE: description = task.description var idx := description.find("\n") description = description.substr(0, idx) ctx.settings.DescriptionOnBoard.UNTIL_FIRST_BLANK_LINE: description = task.description var idx := description.find("\n\n") description = description.substr(0, idx) _: description = task.description description_label.text = description if ctx.settings.max_displayed_lines_in_description > 0: description_label.max_lines_visible = ctx.settings.max_displayed_lines_in_description else: description_label.max_lines_visible = -1 description_label.visible = ctx.settings.show_description_preview and description_label.text.strip_edges().length() != 0 else: description_label.text = "" description_label.visible = (description_label.text.length() > 0) __update_step_holder() var steps := board_data.get_task(data_uuid).steps for step in steps: if not step.changed.is_connected(__update_step_holder): step.changed.connect(__update_step_holder) if title_label.text_changed.is_connected(__set_title): title_label.text_changed.disconnect(__set_title) title_label.text = board_data.get_task(data_uuid).title title_label.text_changed.connect(__set_title) __update_category_menu() __update_category_button() __update_tooltip() queue_redraw() func show_edit(intention: __EditLabel.INTENTION) -> void: title_label.show_edit(intention) func __update_step_holder() -> void: var ctx: __EditContext = __Singletons.instance_of(__EditContext, self) var task := board_data.get_task(data_uuid) var expanded := expand_button.expanded step_holder.clear_steps() var step_count := 0 var expandable := false if ctx.settings.show_steps_preview: var steps := board_data.get_task(data_uuid).steps var max_step_count := ctx.settings.max_steps_on_board match ctx.settings.steps_on_board: ctx.settings.StepsOnBoard.ONLY_OPEN: for i in steps.size(): if steps[i].done: continue if max_step_count > 0 and step_count >= max_step_count: expandable = true if not expanded: break step_holder.add_step(steps[i]) step_count += 1 ctx.settings.StepsOnBoard.ALL_OPEN_FIRST: for i in steps.size(): if steps[i].done: continue if max_step_count > 0 and step_count >= max_step_count: expandable = true if not expanded: break step_holder.add_step(steps[i]) step_count += 1 for i in steps.size(): if not steps[i].done: continue if max_step_count > 0 and step_count >= max_step_count: expandable = true if not expanded: break step_holder.add_step(steps[i]) step_count += 1 ctx.settings.StepsOnBoard.ALL_IN_ORDER: for i in steps.size(): if max_step_count > 0 and step_count >= max_step_count: expandable = true if not expanded: break step_holder.add_step(steps[i]) step_count += 1 _: pass step_holder.visible = (step_count > 0) expand_button.visible = expandable func __update_category_menu() -> void: __category_menu.board_data = board_data func __update_category_button() -> void: if board_data.get_category_count() > 1: category_button.mouse_filter = Control.MOUSE_FILTER_STOP else: category_button.mouse_filter = Control.MOUSE_FILTER_IGNORE func __update_tooltip() -> void: var ctx: __EditContext = __Singletons.instance_of(__EditContext, self) var task := board_data.get_task(data_uuid) var task_category := board_data.get_category(task.category) var steps := board_data.get_task(data_uuid).steps var category_bullet = "[bgcolor=#" + task_category.color.to_html(false) + "] [/bgcolor] " #var category_bullet = "[color=#" + task_category.color.to_html(false) + "]\u2588\u2588[/color]" #var category_bullet = "[color=#" + task_category.color.to_html(false) + "]\u220E[/color]" #var category_bullet = "[color=#" + task_category.color.to_html(false) + "]\u25A0[/color]" tooltip_text = category_bullet + " " + board_data.get_category(task.category).title + ": " + task.title if task.description !=null and task.description.length() > 0: tooltip_text += "[p]" + task.description + "[/p]" var open_steps = [] var done_steps = [] for step in steps: (done_steps if step.done else open_steps).append(step) #var open_step_bullet = "\u25A1" # Unfilled square #var open_step_bullet = "[color=#808080]\u25A0[/color]" # Filled gray square #var done_step_bullet = "\u25A0" # Filled square #var open_step_bullet = "\u2718" # Heavy ballot X #var done_step_bullet = "\u2714" # Heavy check mark #var open_step_bullet = "\u2717" # Ballot X #var done_step_bullet = "\u2713" # Check mark var open_step_bullet = "[color=#F08080]\u25A0[/color]" # Filled red square var done_step_bullet = "[color=#98FB98]\u25A0[/color]" # Filled green square if open_steps.size() > 0 or done_steps.size() > 0: tooltip_text += "[p]" if open_steps.size() > 0: tooltip_text += "Open steps:\n[table=2]" for step in open_steps: tooltip_text += "[cell]" + open_step_bullet + "[/cell][cell]" + step.details + "[/cell]\n" tooltip_text += "[/table]\n" if done_steps.size() > 0: tooltip_text += "Done steps:\n[table=2]" for step in done_steps: tooltip_text += "[cell]" + done_step_bullet + "[/cell][cell]" + step.details + "[/cell]\n" tooltip_text += "[/table]\n" tooltip_text += "[/p]" func __apply_filter() -> void: var ctx: __EditContext = __Singletons.instance_of(__EditContext, self) if not ctx.filter or ctx.filter.text.length() == 0: show() return var task = board_data.get_task(data_uuid) var filter_simple := __simplify_string(ctx.filter.text) var filter_matches := false if not filter_matches: var text_simple := __simplify_string(task.title) if text_simple.matchn("*" + filter_simple + "*"): filter_matches = true if not filter_matches: var category = board_data.get_category(task.category) var text_simple := __simplify_string(category.title) if text_simple.matchn("*" + filter_simple + "*"): filter_matches = true if not filter_matches and ctx.filter.advanced: var text_simple := __simplify_string(task.description) if text_simple.matchn("*" + filter_simple + "*"): filter_matches = true if not filter_matches and ctx.filter.advanced: for step in task.steps: if not filter_matches: var text_simple := __simplify_string(step.details) if text_simple.matchn("*" + filter_simple + "*"): filter_matches = true else: break if filter_matches: show() else: hide() func __simplify_string(string: String) -> String: return string.replace(" ", "").replace("\t", "") func __update_context_menu() -> void: var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self) context_menu.clear() context_menu.add_item("Details", ACTIONS.DETAILS) context_menu.add_separator() context_menu.add_icon_item(get_theme_icon(&"Rename", &"EditorIcons"), "Rename", ACTIONS.RENAME) context_menu.set_item_shortcut(context_menu.get_item_index(ACTIONS.RENAME), shortcuts.rename) context_menu.add_icon_item(get_theme_icon(&"Duplicate", &"EditorIcons"), "Duplicate", ACTIONS.DUPLICATE) context_menu.set_item_shortcut(context_menu.get_item_index(ACTIONS.DUPLICATE), shortcuts.duplicate) context_menu.add_icon_item(get_theme_icon(&"Remove", &"EditorIcons"), "Delete", ACTIONS.DELETE) context_menu.set_item_shortcut(context_menu.get_item_index(ACTIONS.DELETE), shortcuts.delete) func __action(action) -> void: var undo_redo: UndoRedo = __Singletons.instance_of(__EditContext, self).undo_redo match(action): ACTIONS.DELETE: var task = board_data.get_task(data_uuid) for uuid in board_data.get_stages(): var tasks := board_data.get_stage(uuid).tasks if data_uuid in tasks: tasks.erase(data_uuid) undo_redo.create_action("Delete task") undo_redo.add_do_property(board_data.get_stage(uuid), &"tasks", tasks) undo_redo.add_do_method(board_data.remove_task.bind(data_uuid, true)) undo_redo.add_undo_method(board_data.__add_task.bind(task, data_uuid)) undo_redo.add_undo_property(board_data.get_stage(uuid), &"tasks", board_data.get_stage(uuid).tasks) undo_redo.add_undo_reference(task) undo_redo.commit_action() break ACTIONS.DETAILS: details.board_data = board_data details.data_uuid = data_uuid details.popup_centered_ratio_no_fullscreen(0.5) ACTIONS.DUPLICATE: var copy := __TaskData.new() copy.from_json(board_data.get_task(data_uuid).to_json()) var copy_uuid := board_data.add_task(copy) for uuid in board_data.get_stages(): var tasks := board_data.get_stage(uuid).tasks if data_uuid in tasks: tasks.insert(tasks.find(data_uuid), copy_uuid) undo_redo.create_action("Duplicate task") undo_redo.add_do_method(board_data.__add_task.bind(copy, copy_uuid)) undo_redo.add_do_property(board_data.get_stage(uuid), &"tasks", tasks) undo_redo.add_undo_property(board_data.get_stage(uuid), &"tasks", board_data.get_stage(uuid).tasks) undo_redo.add_undo_method(board_data.remove_task.bind(copy_uuid)) undo_redo.commit_action(false) board_data.get_stage(uuid).tasks = tasks break ACTIONS.RENAME: if context_menu.visible: await context_menu.popup_hide title_label.show_edit() func __set_title(value: String) -> void: board_data.get_task(data_uuid).title = value func __on_category_button_pressed() -> void: __category_menu.popup_at_local_position(category_button, Vector2(0, category_button.size.y)) func __on_category_menu_uuid_selected(category_uuid) -> void: var task = board_data.get_task(data_uuid) task.category = category_uuid