Added Kanban plugin

This commit is contained in:
JKuijperM 2024-03-22 17:11:54 +01:00
parent 58e0c84523
commit eb236c5426
58 changed files with 5588 additions and 0 deletions

View File

@ -0,0 +1,19 @@
Copyright (c) 2022-2024 HolonProduction
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,305 @@
@tool
extends "kanban_resource.gd"
## Manages the loading and saving of other data.
const __UUID := preload("../uuid/uuid.gd")
const __Category := preload("category.gd")
const __Layout := preload("layout.gd")
const __Stage := preload("stage.gd")
const __Task := preload("task.gd")
const __KanbanResource := preload("kanban_resource.gd")
var layout: __Layout:
set(value):
if layout:
layout.changed.disconnect(__notify_changed)
layout = value
layout.changed.connect(__notify_changed)
var __categories: Dictionary
var __stages: Dictionary
var __tasks: Dictionary
## Generates a json representation of the board.
func to_json() -> Dictionary:
var dict := {}
var category_data := __propagate_uuid_dict(__categories)
dict["categories"] = category_data
var stage_data := __propagate_uuid_dict(__stages)
dict["stages"] = stage_data
var task_data := __propagate_uuid_dict(__tasks)
dict["tasks"] = task_data
dict["layout"] = layout.to_json()
return dict
## Save the board at `path`.
func save(path: String) -> void:
var file = FileAccess.open(path, FileAccess.WRITE)
if not file:
push_error("Error " + str(FileAccess.get_open_error()) + " while opening file for saving board data at " + path)
file.close()
return
var string := JSON.stringify(to_json(), "\t", false)
file.store_string(string)
file.close()
## Initializes the board state from json data.
func from_json(json: Dictionary) -> void:
__instantiate_uuid_array(json.get("categories", null), __Category, __add_category)
__instantiate_uuid_array(json.get("stages", null), __Stage, __add_stage)
__instantiate_uuid_array(json.get("tasks", null), __Task, __add_task)
layout = __Layout.new([])
if json.get("layout", null) is Dictionary:
layout.from_json(json["layout"])
else:
push_warning("Loading incomplete board data which is missing layout data.")
## Loads the data from `path` into the current instance.
func load(path: String) -> void:
var file = FileAccess.open(path, FileAccess.READ)
if not file:
push_error("Error " + str(FileAccess.get_open_error()) + " while opening file for loading board data at " + path)
file.close()
return
var json = JSON.new()
var err = json.parse(file.get_as_text())
file.close()
if err != OK:
push_error("Error " + str(err) + " while parsing board at " + path + " to json. At line " + str(json.get_error_line()) + " the following problem occured:\n" + json.get_error_message())
return
if json.data.has("columns"):
__from_legacy_file(json.data)
else:
from_json(json.data)
## Adds a category and returns the uuid which is associated with it.
func add_category(category: __Category, silent: bool = false) -> String:
var res := __add_category(category)
if not silent:
__notify_changed()
return res
## Returns the category associated with the given uuid or `null` if there is none.
func get_category(uuid: String) -> __Category:
if not __categories.has(uuid) and uuid != "":
push_warning('There is no category with the uuid "' + uuid + '".')
return __categories.get(uuid, null)
## Returns the count of categories.
func get_category_count() -> int:
return len(__categories)
## Returns the uuid's of all categories.
func get_categories() -> Array[String]:
var temp: Array[String] = []
temp.assign(__categories.keys())
return temp
## Removes a category by uuid.
func remove_category(uuid: String, silent: bool = false) -> void:
if __categories.has(uuid):
__categories[uuid].changed.disconnect(__notify_changed)
__categories.erase(uuid)
if not silent:
__notify_changed()
else:
push_warning("Trying to remove uuid wich is not associated with a category.")
## Adds a stage and returns the uuid which is associated with it.
func add_stage(stage: __Stage, silent: bool = false) -> String:
var res := __add_stage(stage)
if not silent:
__notify_changed()
return res
## Returns the stage associated with the given uuid or `null` if there is none.
func get_stage(uuid: String) -> __Stage:
if not __stages.has(uuid) and uuid != "":
push_warning('There is no stage with the uuid "' + uuid + '".')
return __stages.get(uuid, null)
## Returns the count of stages.
func get_stage_count() -> int:
return len(__stages)
## Returns the uuid's of all stages.
func get_stages() -> Array[String]:
var temp: Array[String] = []
temp.assign(__stages.keys())
return temp
## Removes a stage by uuid.
func remove_stage(uuid: String, silent: bool = false) -> void:
if __stages.has(uuid):
__stages[uuid].changed.disconnect(__notify_changed)
__stages.erase(uuid)
if not silent:
__notify_changed()
else:
push_warning("Trying to remove uuid wich is not associated with a stage.")
## Adds a task and returns the uuid which is associated with it.
func add_task(task: __Task, silent: bool = false) -> String:
var res := __add_task(task)
if not silent:
__notify_changed()
return res
## Returns the task associated with the given uuid or `null` if there is none.
func get_task(uuid: String) -> __Task:
if not __tasks.has(uuid) and uuid != "":
push_warning('There is no task with the uuid "' + uuid + '".')
return __tasks.get(uuid, null)
## Returns the count of tasks.
func get_task_count() -> int:
return len(__tasks)
## Returns the uuid's of all tasks.
func get_tasks() -> Array[String]:
var temp: Array[String] = []
temp.assign(__tasks.keys())
return temp
## Removes a task by uuid.
func remove_task(uuid: String, silent: bool = false) -> void:
if __tasks.has(uuid):
if __tasks[uuid].changed.is_connected(__notify_changed):
__tasks[uuid].changed.disconnect(__notify_changed)
__tasks.erase(uuid)
if not silent:
__notify_changed()
else:
push_warning("Trying to remove uuid wich is not associated with a task.")
# Internal version of `add_category` which can be provided with an uuid suggestion.
# The uuid that is passed can be altered by the board if it is already used by
# an other category. Therefore always use the returned uuid.
func __add_category(category: __Category, uuid: String = "") -> String:
category.changed.connect(__notify_changed)
if __categories.has(uuid):
push_warning("The uuid " + uuid + ' is already used. A new one will be generated for the category "' + category.title + '".')
if uuid == "":
uuid = __UUID.v4()
while uuid in __categories.keys():
uuid = __UUID.v4()
__categories[uuid] = category
return uuid
# Internal version of `add_stage` which can be provided with an uuid suggestion.
func __add_stage(stage: __Stage, uuid: String = "") -> String:
stage.changed.connect(__notify_changed)
if __stages.has(uuid):
push_warning("The uuid " + uuid + ' is already used. A new one will be generated for the stage "' + stage.title + '".')
if uuid == "":
uuid = __UUID.v4()
while uuid in __stages.keys():
uuid = __UUID.v4()
__stages[uuid] = stage
return uuid
# Internal version of `add_task` which can be provided with an uuid suggestion.
func __add_task(task: __Task, uuid: String = "") -> String:
task.changed.connect(__notify_changed)
if __tasks.has(uuid):
push_warning("The uuid " + uuid + ' is already used. A new one will be generated for the task "' + task.title + '".')
if uuid == "":
uuid = __UUID.v4()
while uuid in __tasks.keys():
uuid = __UUID.v4()
__tasks[uuid] = task
return uuid
# HACK: `array` should have the type `Array` but then `null` could not be passed.
func __instantiate_uuid_array(array, type: Script, add_callback: Callable) -> void:
if array == null:
push_warning("Loading incomplete board data which is missing data for '" + type.resource_path + "'.")
return
for data in array:
var instance: __KanbanResource = type.new()
instance.from_json(data)
add_callback.call(instance, data.get("uuid", ""))
# Converts a dictionary with (uuid, kanban_resource) pairs into a list
# json representations with the uuid added.
func __propagate_uuid_dict(dict: Dictionary) -> Array:
var res := []
for key in dict.keys():
var json: Dictionary = {"uuid": key}
json.merge(dict[key].to_json())
res.append(json)
return res
# TODO: Remove this sometime in the future.
## Loads a board from the old file format.
func __from_legacy_file(data: Dictionary) -> void:
var categories: Array[String] = []
var tasks: Array[String] = []
var stages: Array[String] = []
for c in data["categories"]:
categories.append(
__add_category(__Category.new(c["title"], c["color"])),
)
for t in data["tasks"]:
tasks.append(
__add_task(
__Task.new(t["title"], t["details"], categories[t["category"]]),
),
)
for s in data["stages"]:
var contained_tasks: Array[String] = []
for t in s["tasks"]:
contained_tasks.append(tasks[t])
stages.append(
__add_stage(
__Stage.new(s["title"], contained_tasks),
),
)
var columns: Array[PackedStringArray] = []
for c in data["columns"]:
var column = PackedStringArray([])
for s in c["stages"]:
column.append(stages[s])
columns.append(column)
layout = __Layout.new(columns)

View File

@ -0,0 +1,43 @@
@tool
extends "kanban_resource.gd"
## Data of a category.
var title: String:
set(value):
title = value
__notify_changed()
var color: Color:
set(value):
color = value
__notify_changed()
func _init(p_title: String = "", p_color: Color = Color()) -> void:
title = p_title
color = p_color
super._init()
func to_json() -> Dictionary:
return {
"title": title,
"color": color.to_html(false),
}
func from_json(json: Dictionary) -> void:
title = "Missing data."
color = Color.CORNFLOWER_BLUE
if json.has("title"):
title = json["title"]
else:
push_warning("Loading incomplete json data which is missing a title.")
if json.has("color"):
color = Color.html(json["color"])
else:
push_warning("Loading incomplete json data which is missing a color.")

View File

@ -0,0 +1,30 @@
@tool
extends RefCounted
## Base class for kanban tasks data structures.
## Emitted when the resource changed. The properties are updated before emitting.
signal changed()
var __emit_changed := true
func _init() -> void:
pass
## Serializes the object as json.
func to_json() -> Dictionary:
push_error("Method to_json not implemented.")
return {}
## Deserializes the object from json.
func from_json(json: Dictionary) -> void:
push_error("Method from_json not implemented.")
func __notify_changed() -> void:
if __emit_changed:
changed.emit()

View File

@ -0,0 +1,49 @@
@tool
extends "kanban_resource.gd"
## Layout data.
# Use `PackedStringArray` because nested typed collections are not supported.
var columns: Array[PackedStringArray] = []:
get:
return columns.duplicate()
set(value):
columns = value
__notify_changed()
func _init(p_columns: Array[PackedStringArray] = []) -> void:
columns = p_columns
super._init()
func to_json() -> Dictionary:
var cols := []
for c in columns:
var col = []
for uuid in c:
col.append(uuid)
cols.append(col)
return {
"columns": cols,
}
func from_json(json: Dictionary) -> void:
if json.has("columns"):
if json["columns"] is Array:
var cols: Array[PackedStringArray] = []
for c in json["columns"]:
var arr := PackedStringArray()
if c is Array:
for id in c:
arr.append(id)
else:
push_warning("Layout data is corrupted.")
cols.append(arr)
columns = cols
else:
push_warning("Layout data is corrupted.")
else:
push_warning("Loading incomplete json data which is missing a list of columns.")

View File

@ -0,0 +1,151 @@
@tool
extends "kanban_resource.gd"
## Contains settings that are not bound to a board.
const DEFAULT_EDITOR_DATA_PATH: String = "res://kanban_tasks_data.kanban"
enum DescriptionOnBoard {
FULL,
FIRST_LINE,
UNTIL_FIRST_BLANK_LINE,
}
enum StepsOnBoard {
ONLY_OPEN,
ALL_OPEN_FIRST,
ALL_IN_ORDER
}
## Whether the first line of the description is shown on the board.
var show_description_preview: bool = true:
set(value):
show_description_preview = value
__notify_changed()
var show_steps_preview: bool = true:
set(value):
show_steps_preview = value
__notify_changed()
var show_category_on_board: bool = false:
set(value):
show_category_on_board = value
__notify_changed()
var edit_step_details_exclusively: bool = false:
set(value):
edit_step_details_exclusively = value
__notify_changed()
var max_displayed_lines_in_description: int = 0:
set(value):
max_displayed_lines_in_description = value
__notify_changed()
var description_on_board := DescriptionOnBoard.FIRST_LINE:
set(value):
description_on_board = value
__notify_changed()
var steps_on_board := StepsOnBoard.ONLY_OPEN:
set(value):
steps_on_board = value
__notify_changed()
var max_steps_on_board: int = 2:
set(value):
max_steps_on_board = value
__notify_changed()
var editor_data_file_path: String = DEFAULT_EDITOR_DATA_PATH:
set(value):
editor_data_file_path = value
__notify_changed()
var warn_about_empty_deletion: bool = false:
set(value):
warn_about_empty_deletion = value
__notify_changed()
var recent_file_count: int = 5:
set(value):
recent_file_count = value
recent_files.resize(value)
__notify_changed()
var recent_files: PackedStringArray = []:
get:
return recent_files.duplicate()
set(value):
recent_files = value
__notify_changed()
# Here such settings can come, which is own responsibiity of a user control.
# When it just want to persist its own state, but the setting is not used by anything else.
# In this case there is no need to mess up this class with bolerplate code
# E.g. the splitter position in the details editor window
# Set via set_internal_state to trigger notification
# (As no clean-up, during develolpment some mess can remain in it.
# Use clear or erase in your code in such cases, just don't forget there)
var internal_states: Dictionary = { }
func set_internal_state(property: String, value: Variant) -> void:
internal_states[property] = value
__notify_changed()
func to_json() -> Dictionary:
var res := {
"show_description_preview": show_description_preview,
"warn_about_empty_deletion": warn_about_empty_deletion,
"edit_step_details_exclusively": edit_step_details_exclusively,
"max_displayed_lines_in_description": max_displayed_lines_in_description,
"description_on_board": description_on_board,
"show_steps_preview": show_steps_preview,
"show_category_on_board": show_category_on_board,
"steps_on_board": steps_on_board,
"max_steps_on_board": max_steps_on_board,
}
if not Engine.is_editor_hint():
res["recent_file_count"] = recent_file_count
res["recent_files"] = recent_files
else:
res["editor_data_file_path"] = editor_data_file_path
res["internal_states"] = internal_states
return res
func from_json(json: Dictionary) -> void:
if json.has("show_description_preview"):
show_description_preview = json["show_description_preview"]
if json.has("warn_about_empty_deletion"):
warn_about_empty_deletion = json["warn_about_empty_deletion"]
if json.has("edit_step_details_exclusively"):
edit_step_details_exclusively = json["edit_step_details_exclusively"]
if json.has("max_displayed_lines_in_description"):
max_displayed_lines_in_description = json["max_displayed_lines_in_description"]
if json.has("description_on_board"):
description_on_board = json["description_on_board"]
if json.has("editor_data_file_path"):
editor_data_file_path = json["editor_data_file_path"]
if json.has("recent_file_count"):
recent_file_count = json["recent_file_count"]
if json.has("recent_files"):
recent_files = PackedStringArray(json["recent_files"])
if json.has("show_steps_preview"):
show_steps_preview = json["show_steps_preview"]
if json.has("steps_on_board"):
steps_on_board = json["steps_on_board"]
if json.has("max_steps_on_board"):
max_steps_on_board = json["max_steps_on_board"]
if json.has("show_category_on_board"):
show_category_on_board = json["show_category_on_board"]
if json.has("internal_states"):
internal_states = json["internal_states"]
__notify_changed()

View File

@ -0,0 +1,48 @@
@tool
extends "kanban_resource.gd"
## Data of a stage.
var title: String:
set(value):
title = value
__notify_changed()
var tasks: Array[String] = []:
get:
# Pass by value to avoid appending without emitting `changed`.
return tasks.duplicate()
set(value):
tasks = value
__notify_changed()
func _init(p_title: String = "", p_tasks: Array[String] = []) -> void:
title = p_title
tasks = p_tasks
super._init()
func to_json() -> Dictionary:
return {
"title": title,
"tasks": tasks,
}
func from_json(json: Dictionary) -> void:
if json.has("title"):
title = json["title"]
else:
push_warning("Loading incomplete json data which is missing a title.")
if json.has("tasks"):
# HACK: Workaround for casting to typed array.
var s: Array[String] = []
for i in json["tasks"]:
s.append(i)
tasks = s
else:
push_warning("Loading incomplete json data which is missing a list of tasks.")

View File

@ -0,0 +1,40 @@
@tool
extends "kanban_resource.gd"
## Data of a step.
var details: String:
set(value):
details = value
__notify_changed()
var done: bool:
set(value):
done = value
__notify_changed()
func _init(p_details: String = "", p_done: bool = false) -> void:
details = p_details
done = p_done
super._init()
func to_json() -> Dictionary:
return {
"details": details,
"done": done,
}
func from_json(json: Dictionary) -> void:
if json.has("details"):
details = json["details"]
else:
push_warning("Loading incomplete json data which is missing details.")
if json.has("done"):
done = json["done"]
else:
push_warning("Loading incomplete json data which is missing 'done'.")

View File

@ -0,0 +1,86 @@
@tool
extends "kanban_resource.gd"
## Data of a task.
const __Step := preload("step.gd")
var title: String:
set(value):
title = value
__notify_changed()
var description: String:
set(value):
description = value
__notify_changed()
var category: String:
set(value):
category = value
__notify_changed()
var steps: Array[__Step]:
get:
return steps.duplicate()
set(value):
steps = value
__notify_changed()
func _init(p_title: String = "", p_description: String = "", p_category: String = "", p_steps: Array[__Step] = []) -> void:
title = p_title
description = p_description
category = p_category
steps = p_steps
super._init()
func add_step(step: __Step, silent: bool = false) -> void:
var new_steps = steps
new_steps.append(step)
steps = new_steps
step.changed.connect(__notify_changed)
if not silent:
__notify_changed()
func to_json() -> Dictionary:
var s: Array[Dictionary] = []
for step in steps:
s.append(step.to_json())
return {
"title": title,
"description": description,
"category": category,
"steps": s,
}
func from_json(json: Dictionary) -> void:
if json.has("title"):
title = json["title"]
else:
push_warning("Loading incomplete json data which is missing a title.")
if json.has("description"):
description = json["description"]
else:
push_warning("Loading incomplete json data which is missing a description.")
if json.has("category"):
category = json["category"]
else:
push_warning("Loading incomplete json data which is missing a category.")
if json.has("steps"):
var s: Array[__Step] = []
for step in json["steps"]:
s.append(__Step.new())
s[-1].from_json(step)
s[-1].changed.connect(__notify_changed)
steps = s
else:
push_warning("Loading incomplete json data which is missing steps.")

View File

@ -0,0 +1,192 @@
@tool
extends VBoxContainer
## A label with editable content.
##
## While not editing the text is displayed in a label. So it does not stick out
## of some layout to much. Only while editing the label it is replaced with an
## line edit.
## Click onto the label to start editing it or start the editing mode via code
## by using [member show_edit]. When editing press [kbd]Enter[/kbd] to finish
## editing or press [kbd]Esc[/kbd] to discard your changes.
## Emitted when the text changed.
##
## [b]Note:[/b] This is only emitted when you confirm your editing by pressing
## [kbd]Enter[/kbd]. If you need access to all changes while editing use the
## line edit directly. You can get it by calling [method get_edit].
signal text_changed(new_text: String)
## The intentions with which the label can be edited.
enum INTENTION {
REPLACE, ## The text will be marked completly when editing.
ADDITION, ## The cursor is placed at the end when editing.
}
## The text to display and edit.
@export var text: String = "":
set(value):
text = value
__update_content()
text_changed.emit(text)
## The default intention when editing the [member text].
@export var default_intention := INTENTION.ADDITION
## Whether a double click is needed for editing. If [code]false[/code] a single
## click is enough.
@export var double_click: bool = true
# The line edit which is used to edit the text.
var __edit: LineEdit
# The label which is used to display the text.
var __label: Label
# The node which had focus before editing started. Used
# to give focus back to it, when [kbd]Enter[/kbd] is used.
var __old_focus: Control = null
func _ready() -> void:
alignment = BoxContainer.ALIGNMENT_CENTER
mouse_filter = Control.MOUSE_FILTER_PASS
# Setup the internal label.
__label = Label.new()
__label.size_flags_horizontal = SIZE_EXPAND_FILL
__label.size_flags_vertical = SIZE_SHRINK_CENTER
__label.mouse_filter = Control.MOUSE_FILTER_PASS
__label.clip_text = true
__label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
__label.gui_input.connect(__on_label_gui_input)
add_child(__label)
# Setup the internal line edit.
__edit = LineEdit.new()
__edit.visible = false
__edit.size_flags_horizontal = SIZE_EXPAND_FILL
__edit.size_flags_vertical = SIZE_FILL
__edit.text_submitted.connect(__on_edit_text_submitted)
__edit.gui_input.connect(__on_edit_gui_input)
__edit.focus_exited.connect(__on_edit_focus_exited, CONNECT_DEFERRED)
add_child(__edit)
__update_content()
# Wait for the label to get its true size.
await get_tree().create_timer(0.0).timeout
# Keep the same size when changing the edit mode.
custom_minimum_size.y = max(__label.size.y, __edit.size.y) * 1.1
func _input(event: InputEvent) -> void:
# End the editing when somewhere else was clicked.
if (event is InputEventMouseButton) and event.pressed and __edit.visible:
var local = __edit.make_input_local(event)
if not Rect2(Vector2.ZERO, __edit.size).has_point(local.position):
show_label()
## Start editing the text and pass an optional intention.
## This can be used to open the edit interface via code.
func show_edit(intention: INTENTION = default_intention) -> void:
if __edit.visible:
return
# When this node can grab focus the focus should not be given back to the
# old focus owner.
__old_focus = get_viewport().gui_get_focus_owner() if focus_mode == FOCUS_NONE else null
__update_content()
__label.visible = false
__edit.visible = true
__edit.grab_focus()
match intention:
INTENTION.ADDITION:
__edit.caret_column = len(__edit.text)
INTENTION.REPLACE:
__edit.select_all()
## Ends editing. If [code]apply_changes[/code] is [code]true[/code] the changed
## text will be applied to the own [member text]. Otherwise the changes will
## be discarded.
func show_label(apply_changes: bool = true) -> void:
if __label.visible:
return
if apply_changes:
text = __edit.text
if is_instance_valid(__old_focus):
__old_focus.grab_focus()
else:
if focus_mode == FOCUS_NONE:
__edit.release_focus()
else:
grab_focus()
__edit.visible = false
__label.visible = true
## Returns the [LineEdit] used to edit the text.
##
## [b]Warning:[/b] This is a required internal node, romoving and freeing it
## may cause a crash. Feel free to edit its parameters to change, how the
## [member text] is displayed.
func get_edit() -> LineEdit:
return __edit
## Returns the [Label] used display the text.
##
## [b]Warning:[/b] This is a required internal node, romoving and freeing it
## may cause a crash. Feel free to edit its parameters to change, how the
## [member text] is displayed.
func get_label() -> Label:
return __label
# Updates the diplayed text of [member __edit] and
# [member __label] based on [member text].
func __update_content() -> void:
if __label:
__label.text = text
if __edit:
__edit.text = text
func __on_label_gui_input(event: InputEvent) -> void:
# Edit when the label is clicked.
if event is InputEventMouseButton:
if event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
if double_click == event.is_double_click():
# Mark event as handled.
__label.accept_event()
show_edit()
func __on_edit_gui_input(event: InputEvent) -> void:
# Discard changes if ui_cancel action is pressed.
if event is InputEventKey and event.is_pressed():
if event.is_action(&"ui_cancel"):
show_label(false)
func __on_edit_text_submitted(_new_text: String) -> void:
# For some reason line edit does not accept the event on its own in GD4.
__edit.accept_event()
show_label()
func __on_edit_focus_exited() -> void:
if __edit.visible:
__old_focus = null
show_label()

View File

@ -0,0 +1,39 @@
@tool
extends Button
signal state_changed(expanded: bool)
@export var expanded: bool = true:
set(value):
if value != expanded:
expanded = value
__update_icon()
state_changed.emit(expanded)
var __texture_rect := TextureRect.new()
func _init() -> void:
focus_mode = Control.FOCUS_NONE
flat = true
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
center.add_child(__texture_rect)
pressed.connect(__on_pressed)
text = " "
__update_icon()
func _notification(what) -> void:
if what == NOTIFICATION_THEME_CHANGED:
__texture_rect.texture = get_theme_icon(&"Collapse", &"EditorIcons")
func __update_icon() -> void:
__texture_rect.flip_v = expanded
func __on_pressed() -> void:
expanded = !expanded

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg4"
version="1.1"
width="128"
viewBox="0 0 128 128"
height="128"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<style
id="style833" />
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
id="path2"
fill="#e0e0e0"
style="display:inline;fill-opacity:1;stroke-width:1"
d="m 124.42825,124.42808 c 1.04925,-1.04923 0.52463,-3.67233 -0.52461,-8.91851 l -5.2462,-26.230876 c -1.04923,-5.246181 -1.12134,-5.44896 -4.19694,-8.393751 L 42.06322,8.4876541 C 37.413004,3.8374388 29.925632,3.8374601 25.275439,8.4876504 L 8.4876601,25.275428 c -4.650214,4.650214 -4.650214,12.137563 5e-6,16.787778 L 80.88496,114.46034 c 3.147711,3.14772 3.147711,3.14772 8.393892,4.19694 l 26.230888,5.24619 c 5.24617,1.04921 7.86929,1.57385 8.91851,0.52461 z M 88.229612,96.623329 c -2.31791,2.317909 -6.075982,2.317912 -8.393892,-8e-6 L 46.260169,63.047932 c -2.317902,-2.317902 -2.317917,-6.075987 -7e-6,-8.393897 2.317904,-2.317902 6.075992,-2.317892 8.393895,9e-6 l 33.575566,33.575408 c 2.317898,2.317898 2.317898,6.075975 -1.1e-5,8.393877 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bc22jf62qsikb"
path="res://.godot/imported/icon.svg-055e2d1e864a196c183290847b905009.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/icon.svg"
dest_files=["res://.godot/imported/icon.svg-055e2d1e864a196c183290847b905009.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=0.125
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true

View File

@ -0,0 +1,7 @@
[plugin]
name="Kanban Tasks - Todo Manager"
description="Another kanban board plugin for the godot engine."
author="HolonProduction"
version="2.0"
script="plugin.gd"

View File

@ -0,0 +1,441 @@
@tool
extends "standalone_plugin.gd"
const __Singletons := preload("./plugin_singleton/singletons.gd")
const __Shortcuts := preload("./view/shortcuts.gd")
const __EditContext := preload("./view/edit_context.gd")
const __Settings := preload("./data/settings.gd")
const __BoardData := preload("./data/board.gd")
const __LayoutData := preload("./data/layout.gd")
const __TaskData := preload("./data/task.gd")
const __CategoryData := preload("./data/category.gd")
const __StageData := preload("./data/stage.gd")
const __BoardView := preload("./view/board/board.tscn")
const __BoardViewType := preload("./view/board/board.gd")
const __StartView := preload("./view/start/start.tscn")
const __StartViewType := preload("./view/start/start.gd")
const __DocumentationView := preload("./view/documentation/documentation.tscn")
const SETTINGS_KEY: String = "kanban_tasks/general/settings"
enum {
ACTION_SAVE,
ACTION_SAVE_AS,
ACTION_OPEN,
ACTION_CREATE,
ACTION_CLOSE,
ACTION_DOCUMENTATION,
ACTION_QUIT,
}
var main_panel_frame: MarginContainer
var start_view: __StartViewType
var file_dialog_save: FileDialog
var file_dialog_open: FileDialog
var discard_changes_dialog: ConfirmationDialog
var documentation_dialog: AcceptDialog
var file_menu: PopupMenu
var help_menu: PopupMenu
var board_view: __BoardViewType
var board_label: Label
var board_path: String = "":
set(value):
board_path = value
__update_board_label()
__update_menus()
var board_changed: bool = false:
set(value):
board_changed = value
__update_board_label()
func _enter_tree() -> void:
board_label = Label.new()
if not Engine.is_editor_hint():
add_control_to_container(CONTAINER_TOOLBAR, board_label)
file_menu = PopupMenu.new()
file_menu.name = "File"
file_menu.add_item("Save board", ACTION_SAVE)
file_menu.add_item("Save board as...", ACTION_SAVE_AS)
file_menu.add_item("Close board", ACTION_CLOSE)
file_menu.add_item("Open board...", ACTION_OPEN)
file_menu.add_item("Create board", ACTION_CREATE)
file_menu.add_separator()
file_menu.add_item("Quit", ACTION_QUIT)
file_menu.id_pressed.connect(__action)
add_menu(file_menu)
help_menu = PopupMenu.new()
help_menu.name = "Help"
help_menu.add_item("Documentation", ACTION_DOCUMENTATION)
help_menu.id_pressed.connect(__action)
add_menu(help_menu)
file_dialog_save = FileDialog.new()
file_dialog_save.access = FileDialog.ACCESS_FILESYSTEM
file_dialog_save.file_mode = FileDialog.FILE_MODE_SAVE_FILE
file_dialog_save.add_filter("*.kanban, *.json", "Kanban Board")
file_dialog_save.min_size = Vector2(800, 500)
file_dialog_save.file_selected.connect(__save_board)
get_editor_interface().get_base_control().add_child(file_dialog_save)
file_dialog_open = FileDialog.new()
file_dialog_open.access = FileDialog.ACCESS_FILESYSTEM
file_dialog_open.file_mode = FileDialog.FILE_MODE_OPEN_FILE
file_dialog_open.add_filter("*.kanban, *.json", "Kanban Board")
file_dialog_open.min_size = Vector2(800, 500)
file_dialog_open.file_selected.connect(__open_board)
get_editor_interface().get_base_control().add_child(file_dialog_open)
discard_changes_dialog = ConfirmationDialog.new()
discard_changes_dialog.dialog_text = "All unsaved changes will be discarded."
discard_changes_dialog.unresizable = true
get_editor_interface().get_base_control().add_child(discard_changes_dialog)
documentation_dialog = __DocumentationView.instantiate()
get_editor_interface().get_base_control().add_child(documentation_dialog)
main_panel_frame = MarginContainer.new()
main_panel_frame.add_theme_constant_override(&"margin_top", 5)
main_panel_frame.add_theme_constant_override(&"margin_left", 5)
main_panel_frame.add_theme_constant_override(&"margin_bottom", 5)
main_panel_frame.add_theme_constant_override(&"margin_right", 5)
main_panel_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL
get_editor_interface().get_editor_main_screen().add_child(main_panel_frame)
start_view = __StartView.instantiate()
start_view.create_board.connect(__action.bind(ACTION_CREATE))
start_view.open_board.connect(__on_start_view_open_board)
main_panel_frame.add_child(start_view)
_make_visible(false)
await get_tree().create_timer(0.0).timeout
__load_settings()
if Engine.is_editor_hint():
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
var editor_data_file_path = ctx.settings.editor_data_file_path
if FileAccess.file_exists(editor_data_file_path):
__open_board(editor_data_file_path)
elif FileAccess.file_exists("res://addons/kanban_tasks/data.json"):
# TODO: Remove sometime in the future.
# Migrate from old version.
__open_board("res://addons/kanban_tasks/data.json")
__save_board(editor_data_file_path)
else:
__create_board()
__save_board(editor_data_file_path)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.save_board.connect(__editor_save_board)
ctx.reload_board.connect(__editor_reload_board)
ctx.create_board.connect(__editor_create_board)
__update_menus()
func _exit_tree() -> void:
if not Engine.is_editor_hint():
remove_control_from_container(CONTAINER_TOOLBAR, board_label)
board_label.queue_free()
remove_menu(file_menu)
file_menu.queue_free()
remove_menu(help_menu)
file_menu.queue_free()
file_dialog_save.queue_free()
file_dialog_open.queue_free()
discard_changes_dialog.queue_free()
documentation_dialog.queue_free()
main_panel_frame.queue_free()
start_view.queue_free()
if is_instance_valid(board_view):
board_view.queue_free()
func _shortcut_input(event: InputEvent) -> void:
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
if not Engine.is_editor_hint() and shortcuts.save.matches_event(event):
get_viewport().set_input_as_handled()
__action(ACTION_SAVE)
if not Engine.is_editor_hint() and shortcuts.save_as.matches_event(event):
get_viewport().set_input_as_handled()
__action(ACTION_SAVE_AS)
func _has_main_screen() -> bool:
return true
func _make_visible(visible) -> void:
if main_panel_frame:
main_panel_frame.visible = visible
func _get_plugin_name() -> String:
return "Tasks"
func _get_plugin_icon() -> Texture2D:
return preload("./icon.svg")
func _notification(what: int) -> void:
match what:
NOTIFICATION_WM_CLOSE_REQUEST:
if not Engine.is_editor_hint():
if not board_changed:
get_tree().quit()
else:
__request_discard_changes(get_tree().quit)
func __update_menus() -> void:
if not is_instance_valid(file_menu):
return
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
file_menu.set_item_disabled(
file_menu.get_item_index(ACTION_SAVE),
not is_instance_valid(board_view),
)
file_menu.set_item_shortcut(
file_menu.get_item_index(ACTION_SAVE),
shortcuts.save,
)
file_menu.set_item_disabled(
file_menu.get_item_index(ACTION_SAVE_AS),
not is_instance_valid(board_view),
)
file_menu.set_item_shortcut(
file_menu.get_item_index(ACTION_SAVE_AS),
shortcuts.save_as,
)
file_menu.set_item_disabled(
file_menu.get_item_index(ACTION_CLOSE),
not is_instance_valid(board_view),
)
func __update_board_label() -> void:
if not is_instance_valid(board_label):
return
if is_instance_valid(board_view):
if board_path.is_empty():
board_label.text = "unsaved"
else:
board_label.text = board_path
if board_changed:
board_label.text += "*"
else:
board_label.text = ""
func __action(id: int) -> void:
match id:
ACTION_SAVE:
__request_save()
ACTION_SAVE_AS:
__request_save(true)
ACTION_CREATE:
if not board_changed:
__create_board()
else:
__request_discard_changes(__create_board)
ACTION_OPEN:
if not board_changed:
file_dialog_open.popup_centered()
else:
__request_discard_changes(file_dialog_open.popup_centered)
ACTION_CLOSE:
if not board_changed:
__close_board()
else:
__request_discard_changes(__close_board)
ACTION_DOCUMENTATION:
documentation_dialog.popup_centered()
ACTION_QUIT:
get_tree().get_root().propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
func __editor_save_board() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
__save_board(ctx.settings.editor_data_file_path)
func __editor_reload_board() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
__action(ACTION_SAVE)
__open_board(ctx.settings.editor_data_file_path)
func __editor_create_board() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
__action(ACTION_SAVE)
__create_board()
__save_board(ctx.settings.editor_data_file_path)
func __request_discard_changes(callback: Callable) -> void:
for connection in discard_changes_dialog.confirmed.get_connections():
discard_changes_dialog.confirmed.disconnect(connection["callable"])
discard_changes_dialog.confirmed.connect(callback)
discard_changes_dialog.popup_centered()
func __request_save(force_new_location: bool = false) -> void:
if not is_instance_valid(board_view):
return
if not force_new_location and not board_path.is_empty():
__save_board(board_path)
else:
file_dialog_save.popup_centered()
func __create_board() -> void:
var data := __BoardData.new()
data.layout = __LayoutData.new([
PackedStringArray([data.add_stage(__StageData.new("Todo"))]),
PackedStringArray([data.add_stage(__StageData.new("Doing"))]),
PackedStringArray([data.add_stage(__StageData.new("Done"))]),
])
data.add_category(
__CategoryData.new(
"Task",
get_editor_interface().get_base_control().
get_theme_color(&"accent_color", &"Editor")
)
)
data.changed.connect(__on_board_changed)
__make_board_view_visible(data)
board_path = ""
board_changed = false
func __save_board(path: String) -> void:
if is_instance_valid(board_view):
__add_to_recent_files(path)
board_path = path
board_view.board_data.save(path)
board_changed = false
func __open_board(path: String) -> void:
var data := __BoardData.new()
data.load(path)
data.changed.connect(__on_board_changed)
__make_board_view_visible(data)
__add_to_recent_files(path)
board_path = path
board_changed = false
func __close_board() -> void:
board_view.queue_free()
board_view = null
board_path = ""
board_changed = false
start_view.show()
func __add_to_recent_files(path: String) -> void:
if Engine.is_editor_hint():
return
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
var files = ctx.settings.recent_files
if path in files:
files.remove_at(files.find(path))
files.insert(0, path)
else:
files.insert(0, path)
files.resize(ctx.settings.recent_file_count)
ctx.settings.recent_files = files
func __on_board_changed() -> void:
board_changed = true
if Engine.is_editor_hint():
__request_save()
func __on_start_view_open_board(path: String) -> void:
if path.is_empty():
__action(ACTION_OPEN)
else:
__open_board(path)
func __make_board_view_visible(data: __BoardData) -> void:
if is_instance_valid(board_view):
board_view.queue_free()
board_view = __BoardView.instantiate()
board_view.show_documentation.connect(__action.bind(ACTION_DOCUMENTATION))
board_view.board_data = data
main_panel_frame.add_child(board_view)
start_view.hide()
func __save_settings() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
var data := JSON.stringify(ctx.settings.to_json())
if Engine.is_editor_hint():
get_editor_interface().get_editor_settings().set_setting(
SETTINGS_KEY,
data,
)
else:
ProjectSettings.set_setting(
SETTINGS_KEY,
data,
)
save_project_settings()
func __load_settings() -> void:
var data: String = "{}"
if Engine.is_editor_hint():
var editor_settings = get_editor_interface().get_editor_settings()
if editor_settings.has_setting(SETTINGS_KEY):
data = editor_settings.get_setting(SETTINGS_KEY)
else:
if ProjectSettings.has_setting(SETTINGS_KEY):
data = ProjectSettings.get_setting(SETTINGS_KEY)
var json = JSON.new()
var err = json.parse(data)
if err != OK:
push_error(
"Error "
+ str(err)
+ " while parsing settings. At line "
+ str(json.get_error_line())
+ " the following problem occured:\n"
+ json.get_error_message()
)
return
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.from_json(json.data)
ctx.settings.changed.connect(__save_settings)

View File

@ -0,0 +1,23 @@
extends Object
## Allows the registration of anonymous singletons into the scene tree.
const HOLDER_NAME: String = "PluginSingletons"
static func instance_of(p_script: Script, requester: Node) -> Variant:
var holder: Node = requester.get_tree().get_root().get_node_or_null(HOLDER_NAME)
if not is_instance_valid(holder):
holder = Node.new()
holder.name = HOLDER_NAME
requester.get_tree().get_root().add_child(holder)
for child in holder.get_children():
if child.get_script() == p_script:
return child
var instance: Node = p_script.new()
holder.add_child(instance)
return instance

View File

@ -0,0 +1,37 @@
#@standalone
# The line above is needed for standalone detection. Do not modify it.
@tool
extends EditorPlugin
## The StandalonePlugin implementation for in editor use.
##
## This file violates DRY. It should not contain references to other files
## of standalone plugin. This allows the independent use as editor plugin.
## Additional containers for add_control_to_container
# CONTAINER_TOOLBAR = 0,
# CONTAINER_SPATIAL_EDITOR_MENU = 1,
# CONTAINER_SPATIAL_EDITOR_SIDE_LEFT = 2,
# CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT = 3,
# CONTAINER_SPATIAL_EDITOR_BOTTOM = 4,
# CONTAINER_CANVAS_EDITOR_MENU = 5,
# CONTAINER_CANVAS_EDITOR_SIDE_LEFT = 6,
# CONTAINER_CANVAS_EDITOR_SIDE_RIGHT = 7,
# CONTAINER_CANVAS_EDITOR_BOTTOM = 8,
# CONTAINER_INSPECTOR_BOTTOM = 9,
# CONTAINER_PROJECT_SETTING_TAB_LEFT = 10,
# CONTAINER_PROJECT_SETTING_TAB_RIGHT = 11,
const CONTAINER_LAUNCH_PAD := 12
func save_project_settings() -> int:
return ProjectSettings.save()
func add_menu(menu: PopupMenu) -> void:
pass
func remove_menu(menu: PopupMenu) -> void:
pass

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Xavier Sellier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,41 @@
# Note: The code might not be as pretty it could be, since it's written
# in a way that maximizes performance. Methods are inlined and loops are avoided.
extends Node
const MODULO_8_BIT = 256
static func getRandomInt():
# Randomize every time to minimize the risk of collisions
randomize()
return randi() % MODULO_8_BIT
static func uuidbin():
# 16 random bytes with the bytes on index 6 and 8 modified
return [
getRandomInt(), getRandomInt(), getRandomInt(), getRandomInt(),
getRandomInt(), getRandomInt(), ((getRandomInt()) & 0x0f) | 0x40, getRandomInt(),
((getRandomInt()) & 0x3f) | 0x80, getRandomInt(), getRandomInt(), getRandomInt(),
getRandomInt(), getRandomInt(), getRandomInt(), getRandomInt(),
]
static func v4():
# 16 random bytes with the bytes on index 6 and 8 modified
var b = uuidbin()
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [
# low
b[0], b[1], b[2], b[3],
# mid
b[4], b[5],
# hi
b[6], b[7],
# clock
b[8], b[9],
# clock
b[10], b[11], b[12], b[13], b[14], b[15]
]

View File

@ -0,0 +1,156 @@
@tool
extends VBoxContainer
## The visual representation of a kanban board.
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __Shortcuts := preload("../shortcuts.gd")
const __EditContext := preload("../edit_context.gd")
const __BoardData := preload("../../data/board.gd")
const __StageScript := preload("../stage/stage.gd")
const __StageScene := preload("../stage/stage.tscn")
const __Filter := preload("../filter.gd")
const __SettingsScript := preload("../settings/settings.gd")
signal show_documentation()
var board_data: __BoardData
@onready var search_bar: LineEdit = %SearchBar
@onready var button_advanced_search: Button = %AdvancedSearch
@onready var button_show_categories: Button = %ShowCategories
@onready var button_show_descriptions: Button = %ShowDescriptions
@onready var button_show_steps: Button = %ShowSteps
@onready var button_documentation: Button = %Documentation
@onready var button_settings: Button = %Settings
@onready var column_holder: HBoxContainer = %ColumnHolder
@onready var settings: __SettingsScript = %SettingsView
func _ready():
update()
board_data.layout.changed.connect(update)
settings.board_data = board_data
search_bar.text_changed.connect(__on_filter_changed)
search_bar.text_submitted.connect(__on_search_bar_entered)
button_advanced_search.toggled.connect(__on_filter_changed)
button_show_categories.toggled.connect(__on_show_categories_toggled)
button_show_descriptions.toggled.connect(__on_show_descriptions_toggled)
button_show_steps.toggled.connect(__on_show_steps_toggled)
notification(NOTIFICATION_THEME_CHANGED)
await get_tree().create_timer(0.0).timeout
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.changed.connect(update)
ctx.filter_changed.connect(__on_filter_changed_external)
button_documentation.pressed.connect(func(): show_documentation.emit())
button_documentation.visible = Engine.is_editor_hint()
button_settings.pressed.connect(settings.popup_centered_ratio_no_fullscreen)
func _shortcut_input(event: InputEvent) -> void:
if not __Shortcuts.should_handle_shortcut(self):
return
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if not event.is_echo() and event.is_pressed():
if shortcuts.search.matches_event(event):
search_bar.grab_focus()
get_viewport().set_input_as_handled()
elif shortcuts.undo.matches_event(event):
ctx.undo_redo.undo()
get_viewport().set_input_as_handled()
elif shortcuts.redo.matches_event(event):
ctx.undo_redo.redo()
get_viewport().set_input_as_handled()
func _notification(what):
match(what):
NOTIFICATION_THEME_CHANGED:
if is_instance_valid(search_bar):
search_bar.right_icon = get_theme_icon(&"Search", &"EditorIcons")
if is_instance_valid(button_settings):
button_settings.icon = get_theme_icon(&"Tools", &"EditorIcons")
if is_instance_valid(button_documentation):
button_documentation.icon = get_theme_icon(&"Help", &"EditorIcons")
if is_instance_valid(button_advanced_search):
button_advanced_search.icon = get_theme_icon(&"Zoom", &"EditorIcons")
if is_instance_valid(button_show_categories):
button_show_categories.icon = get_theme_icon(&"Rectangle", &"EditorIcons")
if is_instance_valid(button_show_descriptions):
button_show_descriptions.icon = get_theme_icon(&"Script", &"EditorIcons")
if is_instance_valid(button_show_steps):
button_show_steps.icon = get_theme_icon(&"FileList", &"EditorIcons")
func update() -> void:
for column in column_holder.get_children():
column.queue_free()
for column_data in board_data.layout.columns:
var column_scroll = ScrollContainer.new()
column_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
column_scroll.set_v_size_flags(Control.SIZE_EXPAND_FILL)
column_scroll.set_h_size_flags(Control.SIZE_EXPAND_FILL)
var column = VBoxContainer.new()
column.set_v_size_flags(Control.SIZE_EXPAND_FILL)
column.set_h_size_flags(Control.SIZE_EXPAND_FILL)
column_scroll.add_child(column)
column_holder.add_child(column_scroll)
for uuid in column_data:
var stage := __StageScene.instantiate()
stage.board_data = board_data
stage.data_uuid = uuid
column.add_child(stage)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
button_show_categories.set_pressed_no_signal(ctx.settings.show_category_on_board)
button_show_descriptions.set_pressed_no_signal(ctx.settings.show_description_preview)
button_show_steps.set_pressed_no_signal(ctx.settings.show_steps_preview)
# Do not use parameters the method is bound to diffrent signals.
func __on_filter_changed(param1: Variant = null):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if ctx.filter_changed.is_connected(__on_filter_changed_external):
ctx.filter_changed.disconnect(__on_filter_changed_external)
ctx.filter = __Filter.new(search_bar.text, button_advanced_search.button_pressed)
ctx.filter_changed.connect(__on_filter_changed_external)
func __on_search_bar_entered(filter: String):
button_advanced_search.grab_focus()
func __on_filter_changed_external():
search_bar.text = ""
func __on_show_categories_toggled(button_pressed: bool):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.show_category_on_board = button_pressed
func __on_show_descriptions_toggled(button_pressed: bool):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.show_description_preview = button_pressed
func __on_show_steps_toggled(button_pressed: bool):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.show_steps_preview = button_pressed

View File

@ -0,0 +1,89 @@
[gd_scene load_steps=3 format=3 uid="uid://c5dk4lnyiag3w"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/board/board.gd" id="1_p7lf4"]
[ext_resource type="PackedScene" uid="uid://dh1yunmhipirg" path="res://addons/kanban_tasks/view/settings/settings.tscn" id="2_by8mq"]
[node name="BoardView" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 0
theme_override_constants/separation = 5
script = ExtResource("1_p7lf4")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
theme_override_constants/separation = 5
[node name="SearchBar" type="LineEdit" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Search"
clear_button_enabled = true
[node name="AdvancedSearch" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Search in details."
toggle_mode = true
[node name="VSeparator" type="VSeparator" parent="Header"]
layout_mode = 2
tooltip_text = "Show categories"
theme_override_constants/separation = 0
[node name="ShowCategories" type="Button" parent="Header"]
unique_name_in_owner = true
visible = false
layout_mode = 2
toggle_mode = true
[node name="ShowDescriptions" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Show descriptions."
toggle_mode = true
[node name="ShowSteps" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Show steps."
toggle_mode = true
[node name="VSeparator2" type="VSeparator" parent="Header"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="Documentation" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Open documentation."
flat = true
[node name="Settings" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Manage board settings."
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 0
vertical_scroll_mode = 0
[node name="ColumnHolder" type="HBoxContainer" parent="ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 5
alignment = 1
[node name="SettingsView" parent="." instance=ExtResource("2_by8mq")]
unique_name_in_owner = true
visible = false

View File

@ -0,0 +1,44 @@
@tool
extends PopupMenu
const __BoardData := preload("../../data/board.gd")
var board_data: __BoardData
signal uuid_selected(uuid)
func _init() -> void:
about_to_popup.connect(__update_items_from_board)
id_pressed.connect(__on_id_pressed)
func popup_at_local_position(source: CanvasItem, local_position: Vector2) -> void:
popup_at_global_position(source, source.get_global_transform() * local_position)
func popup_at_global_position(source: CanvasItem, global_position: Vector2) -> void:
position = global_position
if not source.get_window().gui_embed_subwindows:
position += source.get_window().position
popup()
func popup_at_mouse_position(source: CanvasItem) -> void:
popup_at_global_position(source, source.get_global_mouse_position())
func __update_items_from_board() -> void:
clear()
size = Vector2i.ZERO
for uuid in board_data.get_categories():
var i = Image.create(16, 16, false, Image.FORMAT_RGB8)
i.fill(board_data.get_category(uuid).color)
var t = ImageTexture.create_from_image(i)
add_icon_item(t, board_data.get_category(uuid).title)
set_item_metadata(-1, uuid)
func __on_id_pressed(id) -> void:
uuid_selected.emit(get_item_metadata(id))

View File

@ -0,0 +1,227 @@
@tool
extends AcceptDialog
const __BoardData := preload("../../data/board.gd")
const __StepData := preload("../../data/step.gd")
const __StepEntry := preload("../details/step_entry.gd")
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __EditContext := preload("../edit_context.gd")
var board_data: __BoardData
var data_uuid: String
var __step_data: __StepData
@onready var category_select: OptionButton = %Category
@onready var h_split_container: HSplitContainer = %HSplitContainer
@onready var description_edit: TextEdit = %Description
@onready var step_holder: VBoxContainer = %StepHolder
@onready var steps_panel_container: PanelContainer = %PanelContainer
@onready var create_step_edit: LineEdit = %CreateStepEdit
@onready var step_details: VBoxContainer = %StepDetails
@onready var close_step_details_button: Button = %CloseStepDetails
@onready var step_edit: TextEdit = %StepEdit
func _ready() -> void:
about_to_popup.connect(__on_about_to_popup)
create_step_edit.text_submitted.connect(__create_step)
close_step_details_button.pressed.connect(__close_step_details)
notification(NOTIFICATION_THEME_CHANGED)
step_holder.entry_action_triggered.connect(__on_step_action_triggered)
step_holder.entry_move_requesed.connect(__step_move_requesed)
visibility_changed.connect(__save_internal_state)
func _notification(what: int) -> void:
match(what):
NOTIFICATION_THEME_CHANGED:
if is_instance_valid(steps_panel_container):
steps_panel_container.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"panel", &"Tree"))
if is_instance_valid(create_step_edit):
create_step_edit.right_icon = get_theme_icon(&"Add", &"EditorIcons")
if is_instance_valid(close_step_details_button):
close_step_details_button.icon = get_theme_icon(&"Close", &"EditorIcons")
func update() -> void:
if description_edit.text_changed.is_connected(__on_description_changed):
description_edit.text_changed.disconnect(__on_description_changed)
if description_edit.text != board_data.get_task(data_uuid).description:
description_edit.text = board_data.get_task(data_uuid).description
description_edit.text_changed.connect(__on_description_changed)
title = "Task Details: " + board_data.get_task(data_uuid).title
if category_select.item_selected.is_connected(__on_category_selected):
category_select.item_selected.disconnect(__on_category_selected)
category_select.clear()
for uuid in board_data.get_categories():
var i = Image.create(16, 16, false, Image.FORMAT_RGB8)
i.fill(board_data.get_category(uuid).color)
var t = ImageTexture.create_from_image(i)
category_select.add_icon_item(t, board_data.get_category(uuid).title)
category_select.set_item_metadata(-1, uuid)
if uuid == board_data.get_task(data_uuid).category:
category_select.select(category_select.item_count - 1)
category_select.item_selected.connect(__on_category_selected)
step_holder.clear_steps()
for step in board_data.get_task(data_uuid).steps:
step_holder.add_step(step)
for entry in step_holder.get_step_entries():
entry.being_edited = (entry.step_data == __step_data)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
step_details.visible = is_instance_valid(__step_data)
description_edit.visible = not (ctx.settings.edit_step_details_exclusively and is_instance_valid(__step_data))
if is_instance_valid(__step_data):
if step_edit.text_changed.is_connected(__on_step_details_changed):
step_edit.text_changed.disconnect(__on_step_details_changed)
step_edit.text = __step_data.details
step_edit.text_changed.connect(__on_step_details_changed)
# Workaround for godotengine/godot#70451
func popup_centered_ratio_no_fullscreen(ratio: float = 0.8) -> void:
var viewport: Viewport = get_parent().get_viewport()
popup(Rect2i(Vector2(viewport.position) + viewport.size / 2.0 - viewport.size * ratio / 2.0, viewport.size * ratio))
func edit_step_details(step: __StepData) -> void:
if is_instance_valid(__step_data):
__step_data.changed.disconnect(update)
__step_data = step
__step_data.changed.connect(update)
update()
step_edit.set_caret_line(step_edit.get_line_count())
step_edit.set_caret_column(len(step_edit.get_line(step_edit.get_line_count() - 1)))
step_edit.grab_focus.call_deferred()
func move_step_up(step: __StepData) -> void:
var steps = board_data.get_task(data_uuid).steps
if step in steps and steps[0] != step:
var index = steps.find(step)
steps.erase(step)
steps.insert(index - 1, step)
board_data.get_task(data_uuid).steps = steps
update()
func move_step_down(step: __StepData) -> void:
var steps = board_data.get_task(data_uuid).steps
if step in steps and steps[-1] != step:
var index = steps.find(step)
steps.erase(step)
steps.insert(index + 1, step)
board_data.get_task(data_uuid).steps = steps
update()
func delete_step(step: __StepData) -> void:
close_step_details(step)
var steps = board_data.get_task(data_uuid).steps
if step in steps:
steps.erase(step)
board_data.get_task(data_uuid).steps = steps
update()
func close_step_details(step: __StepData) -> void:
if __step_data == step:
__close_step_details()
func __on_step_action_triggered(entry: __StepEntry, action: __StepEntry.Actions) -> void:
match action:
__StepEntry.Actions.EDIT_HARD:
edit_step_details(entry.step_data)
__StepEntry.Actions.EDIT_SOFT:
if is_instance_valid(__step_data):
edit_step_details(entry.step_data)
__StepEntry.Actions.CLOSE:
close_step_details(entry.step_data)
__StepEntry.Actions.DELETE:
delete_step(entry.step_data)
__StepEntry.Actions.MOVE_UP:
move_step_up(entry.step_data)
__StepEntry.Actions.MOVE_DOWN:
move_step_down(entry.step_data)
func __step_move_requesed(moved_entry: __StepEntry, target_entry: __StepEntry, move_after_target: bool) -> void:
var steps = board_data.get_task(data_uuid).steps
var moved_idx = steps.find(moved_entry.step_data)
var target_idx = steps.find(target_entry.step_data)
if moved_idx < 0 or target_idx < 0 or moved_idx == target_idx:
return
steps.erase(moved_entry.step_data)
if moved_idx < target_idx:
target_idx -= 1
if move_after_target:
steps.insert(target_idx + 1, moved_entry.step_data)
else:
steps.insert(target_idx, moved_entry.step_data)
board_data.get_task(data_uuid).steps = steps
update()
func __load_internal_state() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if ctx.settings.internal_states.has("details_editor_step_holder_width"):
h_split_container.split_offset = ctx.settings.internal_states["details_editor_step_holder_width"]
func __save_internal_state() -> void:
if not visible:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.set_internal_state("details_editor_step_holder_width", h_split_container.split_offset)
func __close_step_details() -> void:
__step_data.changed.disconnect(update)
__step_data = null
update()
func __on_step_details_changed() -> void:
if __step_data.changed.is_connected(update):
__step_data.changed.disconnect(update)
__step_data.details = step_edit.text
__step_data.changed.connect(update)
func __on_about_to_popup() -> void:
if is_instance_valid(__step_data):
__close_step_details()
update()
__load_internal_state()
if board_data.get_task(data_uuid).description.is_empty():
description_edit.grab_focus.call_deferred()
func __on_description_changed() -> void:
board_data.get_task(data_uuid).description = description_edit.text
func __on_category_selected(index: int) -> void:
board_data.get_task(data_uuid).category = category_select.get_item_metadata(index)
func __create_step(text: String) -> void:
if text.is_empty():
return
var task = board_data.get_task(data_uuid)
var data = __StepData.new(text)
task.add_step(data)
create_step_edit.text = ""
update()
if is_instance_valid(__step_data):
for step in step_holder.get_step_entries():
if step.step_data == data:
step.grab_focus.call_deferred()

View File

@ -0,0 +1,112 @@
[gd_scene load_steps=6 format=3 uid="uid://bwi22eyrmeeet"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/details/details.gd" id="1_gh7s6"]
[ext_resource type="PackedScene" uid="uid://dwjg5vyxx4g48" path="res://addons/kanban_tasks/view/details/step_holder.tscn" id="2_0ptaf"]
[sub_resource type="Image" id="Image_fjijb"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_op8g0"]
image = SubResource("Image_fjijb")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g2k57"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 5.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[node name="Details" type="AcceptDialog"]
title = "Task Details"
size = Vector2i(916, 557)
ok_button_text = "Close"
script = ExtResource("1_gh7s6")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(900, 500)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="Category" type="OptionButton" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="VSplitContainer" type="VSplitContainer" parent="VBoxContainer/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Description" type="TextEdit" parent="VBoxContainer/HSplitContainer/VSplitContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
wrap_mode = 1
[node name="StepDetails" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VSplitContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails"]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Step Details:"
[node name="CloseStepDetails" type="Button" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Close"
icon = SubResource("ImageTexture_op8g0")
flat = true
[node name="StepEdit" type="TextEdit" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
wrap_mode = 1
[node name="StepList" type="VBoxContainer" parent="VBoxContainer/HSplitContainer"]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
[node name="CreateStepEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/StepList"]
unique_name_in_owner = true
layout_mode = 2
placeholder_text = "Create Step"
right_icon = SubResource("ImageTexture_op8g0")
[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/HSplitContainer/StepList"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_g2k57")
[node name="StepHolder" parent="VBoxContainer/HSplitContainer/StepList/PanelContainer" instance=ExtResource("2_0ptaf")]
unique_name_in_owner = true
layout_mode = 2
steps_focus_mode = 2

View File

@ -0,0 +1,135 @@
@tool
extends HBoxContainer
## Visual representation of a step.
signal action_triggered(entry: __StepEntry, action: Actions)
const __EditLabel := preload("../../edit_label/edit_label.gd")
const __StepData := preload("../../data/step.gd")
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __Shortcuts := preload("../shortcuts.gd")
const __StepEntry := preload("step_entry.gd")
enum Actions {
DELETE,
MOVE_UP,
MOVE_DOWN,
EDIT_HARD, ## Forces the step details to open.
EDIT_SOFT, ## Only switches to this step if the details are opened.
CLOSE,
}
@export var context_menu_enabled: bool = true
var done: CheckBox
var title_label: Label
var focus_box: StyleBoxFlat
var context_menu: PopupMenu
var step_data: __StepData
var being_edited := false
func _ready() -> void:
set_h_size_flags(SIZE_EXPAND_FILL)
context_menu = PopupMenu.new()
context_menu.id_pressed.connect(__action)
add_child(context_menu)
done = CheckBox.new()
done.focus_mode = Control.FOCUS_NONE
done.toggled.connect(__set_done)
add_child(done)
title_label = Label.new()
title_label.set_h_size_flags(SIZE_EXPAND_FILL)
title_label.text = step_data.details
title_label.max_lines_visible = 1
title_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
add_child(title_label)
focus_box = StyleBoxFlat.new()
focus_box.bg_color = Color(1, 1, 1, 0.1)
notification(NOTIFICATION_THEME_CHANGED)
step_data.changed.connect(update)
update()
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.rename.matches_event(event):
get_viewport().set_input_as_handled()
__action(Actions.EDIT_HARD)
elif shortcuts.confirm.matches_event(event):
get_viewport().set_input_as_handled()
done.button_pressed = not done.button_pressed
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
accept_event()
if context_menu_enabled:
__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.EDIT_HARD)
func _notification(what) -> void:
match(what):
NOTIFICATION_DRAW:
if has_focus() or being_edited:
focus_box.draw(get_canvas_item(), Rect2(Vector2.ZERO, get_rect().size))
NOTIFICATION_FOCUS_ENTER:
__action(Actions.EDIT_SOFT)
func update() -> void:
tooltip_text = step_data.details
done.set_pressed_no_signal(step_data.done)
title_label.text = step_data.details
func __action(what: Actions) -> void:
action_triggered.emit(self, what)
func __update_context_menu() -> void:
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
context_menu.clear()
context_menu.size = Vector2.ZERO
if being_edited:
context_menu.add_icon_item(get_theme_icon(&"Close", &"EditorIcons"), "Close", Actions.CLOSE)
else:
context_menu.add_icon_item(get_theme_icon(&"Rename", &"EditorIcons"), "Edit", Actions.EDIT_HARD)
context_menu.set_item_shortcut(context_menu.get_item_index(Actions.EDIT_HARD), shortcuts.rename)
context_menu.add_icon_item(get_theme_icon(&"MoveUp", &"EditorIcons"), "Move Up", Actions.MOVE_UP)
context_menu.set_item_disabled(context_menu.get_item_index(Actions.MOVE_UP), get_index() == 0)
context_menu.add_icon_item(get_theme_icon(&"MoveDown", &"EditorIcons"), "Move Down", Actions.MOVE_DOWN)
context_menu.set_item_disabled(context_menu.get_item_index(Actions.MOVE_DOWN), get_index() == get_parent().get_child_count() - 1)
context_menu.add_separator()
context_menu.add_icon_item(get_theme_icon(&"Remove", &"EditorIcons"), "Delete", Actions.DELETE)
func __set_done(done: bool) -> void:
__action(Actions.CLOSE)
step_data.done = done

View File

@ -0,0 +1,184 @@
@tool
extends VBoxContainer
signal entry_action_triggered(entry: __StepEntry, action: __StepEntry.Actions)
signal entry_move_requesed(moved_entry: __StepEntry, target_entry: __StepEntry, move_after_target: bool)
const __StepData := preload("../../data/step.gd")
const __StepEntry := preload("../details/step_entry.gd")
@export var scrollable: bool = true:
set(value):
if value != scrollable:
scrollable = value
__update_children_settings()
@export var steps_can_be_removed: bool = true:
set(value):
if value != steps_can_be_removed:
steps_can_be_removed = value
__update_children_settings()
@export var steps_can_be_reordered: bool = true:
set(value):
if value != steps_can_be_reordered:
steps_can_be_reordered = value
__update_children_settings()
@export var steps_have_context_menu: bool = true:
set(value):
if value != steps_have_context_menu:
steps_have_context_menu = value
__update_children_settings()
@export var steps_focus_mode := FocusMode.FOCUS_NONE:
set(value):
if value != steps_focus_mode:
steps_focus_mode = value
__update_children_settings()
var __mouse_entered_step_list: bool = false
var __move_target_entry: __StepEntry = null
var __move_after_target: bool = false
@onready var __scroll_container: ScrollContainer = %ScrollContainer
@onready var __remove_separator: HSeparator = %RemoveSeparator
@onready var __step_list: VBoxContainer = %StepList
@onready var __remove_area: Button = %RemoveArea
func _ready() -> void:
__remove_area.icon = get_theme_icon(&"Remove", &"EditorIcons")
__step_list.draw.connect(__on_step_list_draw)
__step_list.mouse_exited.connect(__on_step_list_mouse_exited)
__step_list.mouse_entered.connect(__on_step_list_mouse_entered)
__update_children_settings()
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
if not steps_can_be_removed and not steps_can_be_reordered:
return false
if data is __StepEntry:
if __remove_area.get_global_rect().has_point(get_global_transform() * at_position):
return true
__update_move_target(at_position)
return (__move_target_entry != null)
return false
func _get_drag_data(at_position: Vector2) -> Variant:
if not steps_can_be_removed and not steps_can_be_reordered:
return null
for entry in get_step_entries():
if entry.get_global_rect().has_point(get_global_transform() * at_position):
var preview := Label.new()
preview.text = entry.step_data.details
set_drag_preview(preview)
return entry
return null
func _drop_data(at_position: Vector2, data: Variant) -> void:
if __move_target_entry != null:
entry_move_requesed.emit(data, __move_target_entry, __move_after_target)
__move_target_entry = null
if data is __StepEntry:
if __remove_area.get_global_rect().has_point(get_global_transform() * at_position):
data.__action(__StepEntry.Actions.DELETE)
func add_step(step: __StepData) -> void:
var entry = __StepEntry.new()
entry.step_data = step
entry.show_behind_parent = true
entry.size_flags_horizontal = Control.SIZE_EXPAND_FILL
__step_list.add_child(entry)
entry.size_flags_horizontal = Control.SIZE_EXPAND_FILL
entry.action_triggered.connect(__on_entry_action_triggered)
entry.context_menu_enabled = steps_have_context_menu
entry.focus_mode = steps_focus_mode
func clear_steps() -> void:
for step in get_step_entries():
__step_list.remove_child(step)
step.queue_free()
func get_step_entries() -> Array[__StepEntry]:
var step_entries: Array[__StepEntry] = []
if is_instance_valid(__step_list):
for child in __step_list.get_children():
if child is __StepEntry:
step_entries.append(child)
return step_entries
func __update_children_settings() -> void:
if is_instance_valid(__scroll_container):
__scroll_container.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO if scrollable else ScrollContainer.SCROLL_MODE_DISABLED
if is_instance_valid(__remove_separator):
__remove_separator.visible = steps_can_be_removed
if is_instance_valid(__remove_area):
__remove_area.visible = steps_can_be_removed
for entry in get_step_entries():
entry.context_menu_enabled = steps_have_context_menu
entry.focus_mode = steps_focus_mode
func __update_move_target(at_position: Vector2) -> void:
var at_global_position := get_global_transform() * at_position
# This __mouse_entered_step_list is needed here, as this seemed to be the only reliable solution, as:
# 1) something is NOK with transforming at_position to global and compare with step_list.global_rect
# 2) cannot decide what is the visible rect of the step_list
# 3) _can_drop_data was called even after mouse is outside the list (to the bottom direction)
if __mouse_entered_step_list:
var closes_entry: __StepEntry = null
var smallest_distance: float
var position_is_after_closes_entry: bool
for e in get_step_entries():
var entry_global_rect = e.get_global_rect()
var distance := abs(at_global_position.y - entry_global_rect.position.y)
if closes_entry == null or distance < smallest_distance:
closes_entry = e
smallest_distance = distance
position_is_after_closes_entry = false
distance = abs(at_global_position.y - entry_global_rect.end.y)
if closes_entry == null or distance < smallest_distance:
closes_entry = e
smallest_distance = distance
position_is_after_closes_entry = true
__move_target_entry = closes_entry
__move_after_target = position_is_after_closes_entry
else:
__move_target_entry = null
__step_list.queue_redraw()
func __on_step_list_mouse_entered() -> void:
__mouse_entered_step_list = true
func __on_step_list_mouse_exited() -> void:
__mouse_entered_step_list = false
__update_move_target(get_local_mouse_position())
func __on_step_list_draw() -> void:
if __move_target_entry != null:
var target_rect := __step_list.get_global_transform().inverse() * __move_target_entry.get_global_rect()
var separation = __step_list.get_theme_constant(&"separation")
var preview_rect := Rect2(
Vector2(0, target_rect.end.y if __move_after_target else target_rect.position.y - separation),
Vector2(target_rect.size.x, separation)
)
if preview_rect.position.y < 0:
preview_rect.position.y = 0
if preview_rect.end.y > __step_list.size.y:
preview_rect.position.y -= (preview_rect.end.y - __step_list.size.y)
__step_list.draw_rect(preview_rect, get_theme_color(&"step_move_review_color"))
func __on_entry_action_triggered(entry, action) -> void:
entry_action_triggered.emit(entry, action)

View File

@ -0,0 +1,45 @@
[gd_scene load_steps=3 format=3 uid="uid://dwjg5vyxx4g48"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/details/step_holder.gd" id="1_exd17"]
[sub_resource type="Theme" id="Theme_1hs0w"]
StepHolder/base_type = &"VBoxContainer"
StepHolder/colors/step_move_review_color = Color(0.439216, 0.729412, 0.980392, 0.501961)
[node name="StepHolder" type="VBoxContainer"]
offset_right = 326.0
offset_bottom = 500.0
size_flags_horizontal = 3
size_flags_vertical = 3
theme = SubResource("Theme_1hs0w")
theme_type_variation = &"StepHolder"
script = ExtResource("1_exd17")
[node name="ScrollContainer" type="ScrollContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
metadata/_edit_use_anchors_ = true
[node name="StepList" type="VBoxContainer" parent="ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="RemoveSeparator" type="HSeparator" parent="."]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 0
[node name="RemoveArea" type="Button" parent="."]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_vertical = 8
focus_mode = 0
mouse_filter = 2
button_mask = 0
flat = true
icon_alignment = 1

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="1.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="2.8284271"
inkscape:cx="-102.00015"
inkscape:cy="63.462833"
inkscape:window-width="2560"
inkscape:window-height="1361"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid1049" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#575b64;stroke:#575b64;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;paint-order:markers fill stroke;fill-opacity:1;stroke-opacity:1"
id="rect2968"
width="10.583333"
height="23.8125"
x="1.3229166"
y="1.3229166" />
<rect
style="fill:#626771;stroke:#626771;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;paint-order:markers fill stroke;fill-opacity:1;stroke-opacity:1"
id="rect2970"
width="10.583333"
height="23.8125"
x="14.552083"
y="1.3229166" />
<rect
style="fill:#179ceb;fill-opacity:1;stroke:#179ceb;stroke-width:1.05836;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke"
id="rect4591"
width="15.216529"
height="3.9785306"
x="2.3476388"
y="10.695463"
transform="matrix(0.99152896,-0.12988578,0.11857896,0.99294463,0,0)" />
<path
style="fill:#a4c1d3;fill-opacity:0.60851061;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 16.933333,10.847917 c -0.923401,0.09187 -1.777291,0.468265 -2.910417,1.161686 1.103691,-0.186089 2.064921,-0.191401 3.439584,0.16123 l -0.529167,-1.322916"
id="path12560"
sodipodi:nodetypes="cccc" />
<path
id="path12547"
style="color:#000000;fill:#d8d8d8;stroke-linejoin:round;-inkscape-stroke:none;fill-opacity:1"
d="M 16.930749 10.714591 C 16.914596 10.71489 16.89821 10.718029 16.88269 10.72441 C 16.83287 10.74494 16.800315 10.793523 16.800525 10.8474 L 16.800525 11.90625 C 16.80114 11.99606 16.889346 12.059309 16.974674 12.031307 L 17.202051 11.955859 L 17.252693 12.107788 A 0.1825 0.1825 0 0 0 17.48317 12.223026 A 0.1825 0.1825 0 0 0 17.598409 11.992549 L 17.548283 11.841138 L 17.769458 11.767757 C 17.861908 11.735907 17.889068 11.617968 17.820101 11.548649 L 17.027384 10.753865 C 17.001404 10.727637 16.966286 10.713935 16.930749 10.714591 z " />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://3edv1ymvukp0"
path="res://.godot/imported/1.svg-15461a63252c61d47c72a31629894a26.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/view/documentation/1.svg"
dest_files=["res://.godot/imported/1.svg-15461a63252c61d47c72a31629894a26.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=4.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="2.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="5.6568543"
inkscape:cx="-24.483572"
inkscape:cy="14.407301"
inkscape:window-width="2560"
inkscape:window-height="1361"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid1049" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#575b64;stroke:#575b64;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;paint-order:markers fill stroke;fill-opacity:1;stroke-opacity:1"
id="rect2968"
width="10.583333"
height="23.8125"
x="1.3229166"
y="1.3229166" />
<rect
style="fill:#575b64;fill-opacity:1;stroke:#575b64;stroke-width:1.058;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke;stroke-dasharray:none"
id="rect2970"
width="10.583333"
height="10.583331"
x="14.552083"
y="14.552083" />
<rect
style="fill:#575b64;fill-opacity:1;stroke:#575b64;stroke-width:1.05806874;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect16439"
width="10.583333"
height="10.583333"
x="14.552083"
y="1.3229166" />
<rect
style="fill:#179ceb;fill-opacity:1;stroke:#179ceb;stroke-width:1.05807;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect19474"
width="9.5251322"
height="1.3229165"
x="1.8519517"
y="2.9104166" />
<path
style="fill:none;stroke:#106da5;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 2.1166666,3.6049483 H 10.054166"
id="path19478" />
<path
style="fill:#afb7c9;fill-opacity:0.84680849;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 8.4072969,3.5780708 8.1734362,2.7712509 8.7814743,3.4260613 9.1907307,3.0051116 9.295968,3.636536 9.7169177,3.8587037 9.1907307,3.928862 8.968563,4.3147324 8.6996231,3.917169 8.1266639,4.0691784 Z"
id="path19618" />
<path
style="fill:#afb7c9;fill-opacity:0.659574;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 8.2584693,3.4540478 8.0246086,2.6472279 8.6326467,3.3020383 9.0419031,2.8810886 9.1471404,3.512513 9.5680901,3.7346807 9.0419031,3.804839 8.8197354,4.1907094 8.5507955,3.793146 7.9778363,3.9451554 Z"
id="path19622" />
<path
id="path12547"
style="color:#000000;fill:#d8d8d8;fill-opacity:1;stroke-linejoin:round;-inkscape-stroke:none"
d="m 8.8614739,3.5711183 c -0.016153,2.99e-4 -0.032539,0.0034 -0.048059,0.0098 -0.04982,0.02053 -0.082375,0.06911 -0.082165,0.12299 v 1.05885 c 6.15e-4,0.08981 0.088821,0.153059 0.174149,0.125057 l 0.227377,-0.07545 0.050642,0.151929 a 0.1825,0.1825 0 0 0 0.230477,0.115238 0.1825,0.1825 0 0 0 0.115239,-0.230477 l -0.050126,-0.151411 0.221175,-0.07338 c 0.09245,-0.03185 0.11961,-0.149789 0.050643,-0.219108 l -0.792717,-0.794784 c -0.02598,-0.02623 -0.061098,-0.03993 -0.096635,-0.03927 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bbo1gfac2wymg"
path="res://.godot/imported/2.svg-053c5d997067871191f4000b81461e5d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/view/documentation/2.svg"
dest_files=["res://.godot/imported/2.svg-053c5d997067871191f4000b81461e5d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=4.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="3.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="8"
inkscape:cx="36.3125"
inkscape:cy="35.1875"
inkscape:window-width="2560"
inkscape:window-height="1361"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid1049" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#575b64;fill-opacity:1;stroke:#575b64;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke"
id="rect2968"
width="13.262239"
height="18.057812"
x="7.2098951"
y="4.0348959" />
<rect
style="fill:#179ceb;fill-opacity:1;stroke:#179ceb;stroke-width:1.05807;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect19474"
width="9.5251322"
height="1.3229165"
x="9.2602844"
y="22.489584" />
<path
style="fill:none;stroke:#106da5;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 9.5250003,23.184123 H 17.462504"
id="path19478" />
<circle
style="fill:#106da5;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="path23344"
cx="18.654341"
cy="23.18438"
r="0.39464015" />
<path
style="fill:#afb7c9;fill-opacity:0.846809;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 18.36207,23.157246 -0.233861,-0.80682 0.608038,0.65481 0.409257,-0.42095 0.105237,0.631425 0.42095,0.222168 -0.526187,0.07016 -0.222168,0.385871 -0.26894,-0.397564 -0.572959,0.15201 z"
id="path19618" />
<path
id="path12547"
style="color:#000000;fill:#d8d8d8;fill-opacity:1;stroke-linejoin:round;-inkscape-stroke:none"
d="m 18.816247,23.150293 c -0.01615,2.99e-4 -0.03254,0.0034 -0.04806,0.0098 -0.04982,0.02053 -0.08237,0.06911 -0.08216,0.122991 v 1.05885 c 6.15e-4,0.08981 0.08882,0.153059 0.174149,0.125057 l 0.227377,-0.07545 0.05064,0.151929 a 0.1825,0.1825 0 0 0 0.230477,0.115238 0.1825,0.1825 0 0 0 0.11524,-0.230477 l -0.05013,-0.151411 0.221175,-0.07338 c 0.09245,-0.03185 0.11961,-0.149789 0.05064,-0.219108 l -0.792711,-0.794785 c -0.02598,-0.02623 -0.0611,-0.03993 -0.09664,-0.03927 z" />
<rect
style="fill:#646973;fill-opacity:1;stroke:#646973;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke"
id="rect27188"
width="12.336198"
height="1.1451006"
x="7.6729159"
y="4.6529698" />
<rect
style="fill:#646973;fill-opacity:1;stroke:#646973;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke"
id="rect27916"
width="8.2020836"
height="12.687549"
x="7.6729159"
y="7.8279738" />
<rect
style="fill:#646973;fill-opacity:1;stroke:#646973;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke"
id="rect27918"
width="2.2158854"
height="12.687549"
x="17.793228"
y="7.8279738" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bb8knc6dctfj1"
path="res://.godot/imported/3.svg-456dac6515246348acd4893e98f4bdc7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/view/documentation/3.svg"
dest_files=["res://.godot/imported/3.svg-456dac6515246348acd4893e98f4bdc7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=4.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="4.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="8"
inkscape:cx="40.3125"
inkscape:cy="44.4375"
inkscape:window-width="2560"
inkscape:window-height="1361"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid1049" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#d0c268;fill-opacity:1;stroke:#d0c268;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="path32362"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="7.8382812"
sodipodi:cy="7.9705729"
sodipodi:r1="8.6053162"
sodipodi:r2="4.6296601"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-0.94247777"
inkscape:rounded="0.11"
inkscape:randomized="0"
d="m 7.8382814,-0.63474321 c 0.6126837,1e-8 2.2255746,4.49971601 2.7212456,4.85984251 0.495672,0.3601264 5.273567,0.5035881 5.462896,1.0862849 0.18933,0.5826968 -3.591744,3.5071357 -3.781073,4.0898325 -0.18933,0.5826968 1.15068,5.1710773 0.655009,5.5312033 -0.495672,0.360127 -4.4453943,-2.332187 -5.058078,-2.332187 -0.6126837,0 -4.5624065,2.692313 -5.058078,2.332187 C 2.2845315,14.572293 3.6245423,9.9839133 3.4352127,9.4012165 3.245883,8.8185197 -0.53519047,5.8940806 -0.34586078,5.3113838 -0.1565311,4.728687 4.6213638,4.5852256 5.1170353,4.2250992 5.6127068,3.8649727 7.2255977,-0.63474323 7.8382814,-0.63474321 Z"
inkscape:transform-center-y="-0.77805912"
transform="translate(5.3908937,6.0569817)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://phpa3kr3tjwu"
path="res://.godot/imported/4.svg-963688afa901818a04233dc49468c7c7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/view/documentation/4.svg"
dest_files=["res://.godot/imported/4.svg-963688afa901818a04233dc49468c7c7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=4.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,138 @@
[gd_scene load_steps=6 format=3 uid="uid://cwfixtyy5lpin"]
[ext_resource type="Texture2D" uid="uid://3edv1ymvukp0" path="res://addons/kanban_tasks/view/documentation/1.svg" id="1_inmy7"]
[ext_resource type="Texture2D" uid="uid://bbo1gfac2wymg" path="res://addons/kanban_tasks/view/documentation/2.svg" id="2_1g6ul"]
[ext_resource type="Texture2D" uid="uid://bb8knc6dctfj1" path="res://addons/kanban_tasks/view/documentation/3.svg" id="3_e3hha"]
[ext_resource type="Texture2D" uid="uid://phpa3kr3tjwu" path="res://addons/kanban_tasks/view/documentation/4.svg" id="4_ic0nh"]
[sub_resource type="LabelSettings" id="LabelSettings_cmnk1"]
font_size = 20
[node name="AcceptDialog" type="AcceptDialog"]
title = "Documentation"
size = Vector2i(1000, 600)
min_size = Vector2i(1000, 600)
theme_type_variation = &"EditorSettingsDialog"
ok_button_text = "Close"
[node name="Help" type="ScrollContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
horizontal_scroll_mode = 0
[node name="VBoxContainer" type="VBoxContainer" parent="Help"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 10
[node name="PanelContainer1" type="PanelContainer" parent="Help/VBoxContainer"]
layout_mode = 2
theme_type_variation = &"Panel"
[node name="HBoxContainer1" type="HBoxContainer" parent="Help/VBoxContainer/PanelContainer1"]
layout_mode = 2
theme_override_constants/separation = 50
alignment = 2
[node name="Control" type="Control" parent="Help/VBoxContainer/PanelContainer1/HBoxContainer1"]
layout_mode = 2
[node name="Label" type="Label" parent="Help/VBoxContainer/PanelContainer1/HBoxContainer1"]
layout_mode = 2
size_flags_horizontal = 3
text = "A kanban board helps you to organise tasks. After you created a task you can drag and drop it between the stages to reflect its current status. Add spontaneous ideas to the \"Todo\" stage. Move them into \"Doing\" when you are ready to tackle them. Once a task is done move it into \"Done\" to keep track of all your accomplishments."
label_settings = SubResource("LabelSettings_cmnk1")
autowrap_mode = 2
[node name="TextureRect" type="TextureRect" parent="Help/VBoxContainer/PanelContainer1/HBoxContainer1"]
custom_minimum_size = Vector2(200, 200)
layout_mode = 2
texture = ExtResource("1_inmy7")
expand_mode = 1
[node name="PanelContainer2" type="PanelContainer" parent="Help/VBoxContainer"]
layout_mode = 2
theme_type_variation = &"Panel"
[node name="HBoxContainer2" type="HBoxContainer" parent="Help/VBoxContainer/PanelContainer2"]
layout_mode = 2
theme_override_constants/separation = 50
alignment = 2
[node name="TextureRect" type="TextureRect" parent="Help/VBoxContainer/PanelContainer2/HBoxContainer2"]
custom_minimum_size = Vector2(200, 200)
layout_mode = 2
texture = ExtResource("2_1g6ul")
expand_mode = 1
[node name="Label" type="Label" parent="Help/VBoxContainer/PanelContainer2/HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
text = "Boost your productivity by customizing your board!
Double click stage or task names to change them. Configure categories and change the layout in the settings.
Find tasks by using the search bar."
label_settings = SubResource("LabelSettings_cmnk1")
autowrap_mode = 2
[node name="Control" type="Control" parent="Help/VBoxContainer/PanelContainer2/HBoxContainer2"]
layout_mode = 2
[node name="PanelContainer3" type="PanelContainer" parent="Help/VBoxContainer"]
layout_mode = 2
theme_type_variation = &"Panel"
[node name="HBoxContainer3" type="HBoxContainer" parent="Help/VBoxContainer/PanelContainer3"]
layout_mode = 2
theme_override_constants/separation = 50
alignment = 2
[node name="Control" type="Control" parent="Help/VBoxContainer/PanelContainer3/HBoxContainer3"]
layout_mode = 2
[node name="Label" type="Label" parent="Help/VBoxContainer/PanelContainer3/HBoxContainer3"]
layout_mode = 2
size_flags_horizontal = 3
text = "Edit the details of you tasks by clicking the edit button. Give your task a meaningful title and put the details into the description. Give your task a category to keep the overview."
label_settings = SubResource("LabelSettings_cmnk1")
autowrap_mode = 2
[node name="TextureRect" type="TextureRect" parent="Help/VBoxContainer/PanelContainer3/HBoxContainer3"]
custom_minimum_size = Vector2(200, 200)
layout_mode = 2
texture = ExtResource("3_e3hha")
expand_mode = 1
[node name="HBoxContainer4" type="HBoxContainer" parent="Help/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 50
alignment = 1
[node name="Control" type="Control" parent="Help/VBoxContainer/HBoxContainer4"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="Help/VBoxContainer/HBoxContainer4"]
layout_mode = 2
[node name="TextureRect" type="TextureRect" parent="Help/VBoxContainer/HBoxContainer4/HBoxContainer"]
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
texture = ExtResource("4_ic0nh")
expand_mode = 3
[node name="LinkButton" type="LinkButton" parent="Help/VBoxContainer/HBoxContainer4/HBoxContainer"]
layout_mode = 2
size_flags_vertical = 4
text = "Leave a star on Github"
uri = "https://github.com/HolonProduction/godot_kanban_tasks"
[node name="Control2" type="Control" parent="Help/VBoxContainer/HBoxContainer4"]
layout_mode = 2
size_flags_horizontal = 3

View File

@ -0,0 +1,29 @@
@tool
extends Node
## Global stuff for the view system.
const __Filter := preload("filter.gd")
const __SettingData := preload("../data/settings.gd")
signal filter_changed()
signal save_board()
signal create_board()
signal reload_board()
## The currently active filter.
var filter: __Filter = null:
set(value):
filter = value
filter_changed.emit()
## The undo redo for task operations.
var undo_redo := UndoRedo.new()
## uuid of the object that should have focus. This is used to persist focus
## when updating some views.
var focus: String = ""
## Settings that are not tied to the board.
var settings := __SettingData.new()

View File

@ -0,0 +1,14 @@
@tool
extends RefCounted
## A filter configuration for searching tasks.
var text: String
## Whether to search in descriptions.
var advanced: bool
func _init(p_text: String = "", p_advanced: bool = false) -> void:
text = p_text
advanced = p_advanced

View File

@ -0,0 +1,53 @@
@tool
extends VBoxContainer
const __BoardData := preload("../../../data/board.gd")
const __CategoryEntry := preload("../../settings/categories/category_entry.gd")
const __CategoryData := preload("../../../data/category.gd")
const __EditLabel := preload("../../../edit_label/edit_label.gd")
var board_data: __BoardData
var randomizer := RandomNumberGenerator.new()
@onready var category_holder: VBoxContainer = %CategoryHolder
@onready var scroll_container: ScrollContainer = %ScrollContainer
@onready var add_category_button: Button = %Add
func _ready() -> void:
notification(NOTIFICATION_THEME_CHANGED)
randomizer.randomize()
add_category_button.pressed.connect(__add_category)
func _notification(what: int) -> void:
match what:
NOTIFICATION_THEME_CHANGED:
if is_instance_valid(add_category_button):
add_category_button.icon = get_theme_icon(&"Add", &"EditorIcons")
func update() -> void:
for category in category_holder.get_children():
category.queue_free()
for uuid in board_data.get_categories():
var entry := __CategoryEntry.new()
entry.board_data = board_data
entry.data_uuid = uuid
category_holder.add_child(entry)
func __add_category() -> void:
var color = Color.from_hsv(randomizer.randf(), randomizer.randf_range(0.8, 1.0), randomizer.randf_range(0.7, 1.0))
var data = __CategoryData.new("New category", color)
var uuid = board_data.add_category(data)
update()
for i in category_holder.get_children():
if i.data_uuid == uuid:
await get_tree().create_timer(0.0).timeout
i.grab_focus()
i.show_edit(__EditLabel.INTENTION.REPLACE)

View File

@ -0,0 +1,65 @@
[gd_scene load_steps=5 format=3 uid="uid://b2likgss81t0s"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/settings/categories/categories.gd" id="1_n36ev"]
[sub_resource type="Image" id="Image_ig53p"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_w6wkd"]
image = SubResource("Image_ig53p")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gsyou"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 5.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[node name="Categories" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
script = ExtResource("1_n36ev")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="\'Available Categories\'" type="Label" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
text = "Available Categories:"
[node name="Add" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
icon = SubResource("ImageTexture_w6wkd")
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_gsyou")
[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
follow_focus = true
[node name="CategoryHolder" type="VBoxContainer" parent="PanelContainer/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3

View File

@ -0,0 +1,94 @@
@tool
extends HBoxContainer
## Visual representation of a category.
const __EditLabel := preload("../../../edit_label/edit_label.gd")
const __BoardData := preload("../../../data/board.gd")
const __Singletons := preload("../../../plugin_singleton/singletons.gd")
const __Shortcuts := preload("../../shortcuts.gd")
var title: __EditLabel
var delete: Button
var color_picker: ColorPickerButton
var focus_box: StyleBoxFlat
var board_data: __BoardData
var data_uuid: String
func _ready() -> void:
set_h_size_flags(SIZE_EXPAND_FILL)
focus_mode = FOCUS_ALL
title = __EditLabel.new()
title.set_h_size_flags(SIZE_EXPAND_FILL)
title.text = board_data.get_category(data_uuid).title
title.text_changed.connect(__on_title_changed)
add_child(title)
color_picker = ColorPickerButton.new()
color_picker.custom_minimum_size.x = 100
color_picker.edit_alpha = false
color_picker.color = board_data.get_category(data_uuid).color
color_picker.focus_mode = Control.FOCUS_NONE
color_picker.flat = true
color_picker.popup_closed.connect(__on_color_changed)
add_child(color_picker)
delete = Button.new()
delete.focus_mode = FOCUS_NONE
delete.flat = true
delete.disabled = board_data.get_category_count() <= 1
delete.pressed.connect(__on_delete)
add_child(delete)
focus_box = StyleBoxFlat.new()
focus_box.bg_color = Color(1, 1, 1, 0.1)
notification(NOTIFICATION_THEME_CHANGED)
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.rename.matches_event(event):
get_viewport().set_input_as_handled()
title.show_edit()
func _notification(what) -> void:
match(what):
NOTIFICATION_THEME_CHANGED:
if is_instance_valid(delete):
delete.icon = get_theme_icon(&"Remove", &"EditorIcons")
NOTIFICATION_DRAW:
if has_focus():
focus_box.draw(get_canvas_item(), Rect2(Vector2.ZERO, get_rect().size))
func show_edit(intention: int = title.default_intention) -> void:
title.show_edit(intention)
func __on_title_changed(new: String) -> void:
board_data.get_category(data_uuid).title = new
func __on_color_changed() -> void:
board_data.get_category(data_uuid).color = color_picker.color
# Hack to get the tasks to update their color.
board_data.layout.changed.emit()
func __on_delete() -> void:
board_data.remove_category(data_uuid)
var fallback_to = board_data.get_categories()[0]
for uuid in board_data.get_tasks():
if board_data.get_task(uuid).category == data_uuid:
board_data.get_task(uuid).category = fallback_to
get_parent().get_owner().update()

View File

@ -0,0 +1,141 @@
@tool
extends VBoxContainer
const __Singletons := preload("../../../plugin_singleton/singletons.gd")
const __EditContext := preload("../../edit_context.gd")
const __SettingData := preload("../../../data/settings.gd")
var data: __SettingData = null
var file_dialog_open_option: CheckBox
var file_dialog_save_option: CheckBox
var file_dialog_create_option: CheckBox
var file_dialog_option_button_group: ButtonGroup
@onready var show_description_preview: CheckBox = %ShowDescriptionPreview
@onready var show_steps_preview: CheckBox = %ShowStepsPreview
@onready var show_category_on_board: CheckBox = %ShowCategoriesOnBoard
@onready var edit_step_details_exclusively: CheckBox = %EditStepDetailsExclusively
@onready var max_displayed_lines_in_description: SpinBox = %MaxDisplayedLinesInDescription
@onready var description_on_board: OptionButton = %DescriptionOnBoard
# Keep IDs of the items of StepsOnBoard in sync with the values of setting.gd/StepsOnBoard
@onready var steps_on_board: OptionButton = %StepsOnBoard
@onready var max_steps_on_board: SpinBox = %MaxStepsOnBoard
@onready var data_file_path_label: Control = %DataFilePathLabel
@onready var data_file_path_container: Control = %DataFilePathContainer
@onready var data_file_path: LineEdit = %DataFilePath
@onready var data_file_path_button: Button = %DataFilePathButton
@onready var file_dialog: FileDialog = %FileDialog
func _ready() -> void:
await get_tree().create_timer(0.0).timeout
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
data = ctx.settings
data.changed.connect(update)
update()
show_description_preview.toggled.connect(func(x): __apply_changes())
show_steps_preview.toggled.connect(func(x): __apply_changes())
show_category_on_board.toggled.connect(func(x): __apply_changes())
edit_step_details_exclusively.toggled.connect(func(x): __apply_changes())
max_displayed_lines_in_description.value_changed.connect(func(x): __apply_changes())
description_on_board.item_selected.connect(func(x): __apply_changes())
steps_on_board.item_selected.connect(func(x): __apply_changes())
max_steps_on_board.value_changed.connect(func(x): __apply_changes())
if not Engine.is_editor_hint():
data_file_path_label.visible = false
data_file_path_container.visible = false
data_file_path_button.pressed.connect(__open_data_file_path_dialog)
file_dialog_open_option = CheckBox.new()
file_dialog_open_option.text = "Open board from existing file"
file_dialog.get_vbox().add_child(file_dialog_open_option)
file_dialog_save_option = CheckBox.new()
file_dialog_save_option.text = "Save current board to file"
file_dialog.get_vbox().add_child(file_dialog_save_option)
file_dialog_create_option = CheckBox.new()
file_dialog_create_option.text = "Create new board in file"
file_dialog.get_vbox().add_child(file_dialog_create_option)
file_dialog_option_button_group = ButtonGroup.new()
file_dialog_open_option.button_group = file_dialog_option_button_group
file_dialog_save_option.button_group = file_dialog_option_button_group
file_dialog_create_option.button_group = file_dialog_option_button_group
file_dialog_option_button_group.pressed.connect(func (button): __update_file_dialog())
file_dialog.get_line_edit().text_changed.connect(func (new_text): __update_file_dialog())
file_dialog_open_option.button_pressed = true
file_dialog.file_selected.connect(__update_editor_data_file)
func update() -> void:
show_description_preview.button_pressed = data.show_description_preview
show_steps_preview.button_pressed = data.show_steps_preview
show_category_on_board.button_pressed = data.show_category_on_board
edit_step_details_exclusively.button_pressed = data.edit_step_details_exclusively
max_displayed_lines_in_description.value = data.max_displayed_lines_in_description
max_steps_on_board.value = data.max_steps_on_board
description_on_board.select(description_on_board.get_item_index(data.description_on_board))
steps_on_board.select(steps_on_board.get_item_index(data.steps_on_board))
data_file_path.text = data.editor_data_file_path
func __open_data_file_path_dialog() -> void:
file_dialog_open_option.set_pressed_no_signal(true)
file_dialog_save_option.set_pressed_no_signal(false)
file_dialog_create_option.set_pressed_no_signal(false)
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
file_dialog.clear_filters()
file_dialog.add_filter("*.kanban, *.json", "Kanban Board")
file_dialog.popup_centered(file_dialog.size)
func __update_file_dialog() -> void:
if file_dialog_save_option.button_pressed:
file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
file_dialog.title = file_dialog_save_option.text
file_dialog.ok_button_text = "Save"
elif file_dialog_create_option.button_pressed:
file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
file_dialog.title = file_dialog_create_option.text
file_dialog.ok_button_text = "Create"
else:
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
file_dialog.title = file_dialog_open_option.text
file_dialog.ok_button_text = "Open"
func __update_editor_data_file(path: String) -> void:
data_file_path.text = path
__apply_changes()
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if file_dialog_save_option.button_pressed:
ctx.save_board.emit()
elif file_dialog_create_option.button_pressed:
ctx.create_board.emit()
else:
ctx.reload_board.emit()
func __apply_changes() -> void:
if data.changed.is_connected(update):
data.changed.disconnect(update)
data.__emit_changed = false
data.show_description_preview = show_description_preview.button_pressed
data.show_steps_preview = show_steps_preview.button_pressed
data.show_category_on_board = show_category_on_board.button_pressed
data.edit_step_details_exclusively = edit_step_details_exclusively.button_pressed
data.max_displayed_lines_in_description = max_displayed_lines_in_description.value
data.description_on_board = description_on_board.get_selected_id()
data.steps_on_board = steps_on_board.get_selected_id()
data.max_steps_on_board = max_steps_on_board.value
data.editor_data_file_path = data_file_path.text
data.__emit_changed = true
data.__notify_changed()
data.changed.connect(update)

View File

@ -0,0 +1,172 @@
[gd_scene load_steps=3 format=3 uid="uid://due07vdflx4o"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/settings/general/general.gd" id="1_8tblh"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7cqkc"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 5.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[node name="General" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_8tblh")
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_7cqkc")
[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer"]
layout_mode = 2
[node name="GridContainer" type="GridContainer" parent="PanelContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
[node name="ShowDescriptionPreviewLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Show description preview"
[node name="ShowDescriptionPreview" type="CheckBox" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 0
button_pressed = true
text = "On"
[node name="ShowStepsPreviewLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Show steps preview"
[node name="ShowStepsPreview" type="CheckBox" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 0
button_pressed = true
text = "On"
[node name="ShowCategoriesOnBoardLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Show categories on board"
[node name="ShowCategoriesOnBoard" type="CheckBox" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 0
button_pressed = true
text = "On"
[node name="EditStepDetailsExclusivelyLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Edit step details in fullscreen"
[node name="EditStepDetailsExclusively" type="CheckBox" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 0
text = "On"
[node name="DescriptionOnBoardLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
text = "Description on board"
[node name="DescriptionOnBoard" type="OptionButton" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
item_count = 3
selected = 0
popup/item_0/text = "Full description"
popup/item_0/id = 0
popup/item_1/text = "First line of description"
popup/item_1/id = 1
popup/item_2/text = "Until first blank line of description"
popup/item_2/id = 2
[node name="MaxDisplayedLinesInDescriptionLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Maximum displayed lines in description"
[node name="MaxDisplayedLinesInDescription" type="SpinBox" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
value = 2.0
allow_greater = true
[node name="StepsOnBoardLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
text = "Steps on board"
[node name="StepsOnBoard" type="OptionButton" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
item_count = 3
selected = 0
popup/item_0/text = "Only open steps"
popup/item_0/id = 0
popup/item_1/text = "All, but open first"
popup/item_1/id = 1
popup/item_2/text = "All in original order"
popup/item_2/id = 2
[node name="MaxStepsOnBoardLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Maximum steps on board"
[node name="MaxStepsOnBoard" type="SpinBox" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
value = 2.0
allow_greater = true
[node name="DataFilePathLabel" type="Label" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Data file path"
[node name="DataFilePathContainer" type="HBoxContainer" parent="PanelContainer/ScrollContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="DataFilePath" type="LineEdit" parent="PanelContainer/ScrollContainer/GridContainer/DataFilePathContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "res://kanban_tasks_data.kanban"
editable = false
[node name="DataFilePathButton" type="Button" parent="PanelContainer/ScrollContainer/GridContainer/DataFilePathContainer"]
unique_name_in_owner = true
layout_mode = 2
text = " ... "
[node name="FileDialog" type="FileDialog" parent="."]
unique_name_in_owner = true
title = "Open board from existing file"
size = Vector2i(800, 600)
ok_button_text = "Save"
mode_overrides_title = false

View File

@ -0,0 +1,28 @@
@tool
extends AcceptDialog
const __BoardData := preload("../../data/board.gd")
const __CategoriesScene := preload("../settings/categories/categories.tscn")
const __CategoriesScript := preload("../settings/categories/categories.gd")
@onready var category_settings: __CategoriesScript = %Categories
@onready var stage_settings = %Stages
var board_data: __BoardData
func _ready() -> void:
# Wait for board to set board_data.
await get_tree().create_timer(0.0).timeout
category_settings.board_data = board_data
stage_settings.board_data = board_data
about_to_popup.connect(stage_settings.update)
about_to_popup.connect(category_settings.update)
# Workaround for godotengine/godot#70451
func popup_centered_ratio_no_fullscreen(ratio: float = 0.8) -> void:
var viewport: Viewport = get_parent().get_viewport()
popup(Rect2i(Vector2(viewport.position) + viewport.size / 2.0 - viewport.size * ratio / 2.0, viewport.size * ratio))

View File

@ -0,0 +1,39 @@
[gd_scene load_steps=5 format=3 uid="uid://dh1yunmhipirg"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/settings/settings.gd" id="1_4eaw3"]
[ext_resource type="PackedScene" uid="uid://due07vdflx4o" path="res://addons/kanban_tasks/view/settings/general/general.tscn" id="1_dk7pg"]
[ext_resource type="PackedScene" uid="uid://b2likgss81t0s" path="res://addons/kanban_tasks/view/settings/categories/categories.tscn" id="3_iycb0"]
[ext_resource type="PackedScene" uid="uid://dapkpnkm8sow8" path="res://addons/kanban_tasks/view/settings/stages/stages.tscn" id="4_okolg"]
[node name="Settings" type="AcceptDialog"]
title = "Settings"
size = Vector2i(800, 400)
visible = true
theme_type_variation = &"EditorSettingsDialog"
script = ExtResource("1_4eaw3")
[node name="Settings" type="TabContainer" parent="."]
custom_minimum_size = Vector2(600, 300)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="General" parent="Settings" instance=ExtResource("1_dk7pg")]
unique_name_in_owner = true
layout_mode = 2
[node name="Categories" parent="Settings" instance=ExtResource("3_iycb0")]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="Stages" parent="Settings" instance=ExtResource("4_okolg")]
unique_name_in_owner = true
visible = false
layout_mode = 2

View File

@ -0,0 +1,199 @@
@tool
extends VBoxContainer
const __Singletons := preload("../../../plugin_singleton/singletons.gd")
const __EditContext := preload("../../edit_context.gd")
const __BoardData = preload("../../../data/board.gd")
const __StageData = preload("../../../data/stage.gd")
var board_data: __BoardData
var stylebox_n: StyleBoxFlat
var stylebox_hp: StyleBoxFlat
@onready var column_holder: HBoxContainer = %ColumnHolder
@onready var column_add: Button = %AddColumn
@onready var warning_sign: Button = %WarningSign
@onready var warn_about_empty_deletion: CheckBox = %WarnAboutEmptyDeletion
@onready var confirm_not_empty: ConfirmationDialog = %ConfirmNotEmpty
@onready var confirm_empty: ConfirmationDialog = %ConfirmEmpty
@onready var task_destination: OptionButton = %TaskDestination
func _ready() -> void:
column_add.focus_mode = Control.FOCUS_NONE
column_add.pressed.connect(__on_add_stage.bind(-1))
var center_container := CenterContainer.new()
center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
center_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
column_add.add_child(center_container)
var plus := TextureRect.new()
plus.mouse_filter = Control.MOUSE_FILTER_IGNORE
center_container.add_child(plus)
notification(NOTIFICATION_THEME_CHANGED)
await get_tree().create_timer(0.0).timeout
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.changed.connect(__settings_changed)
warn_about_empty_deletion.toggled.connect(__apply_settings_changes)
func _notification(what) -> void:
match(what):
NOTIFICATION_THEME_CHANGED:
stylebox_n = get_theme_stylebox(&"normal", &"Button").duplicate()
stylebox_n.set_border_width_all(1)
stylebox_n.border_color = Color8(32, 32, 32, 255)
stylebox_hp = get_theme_stylebox(&"read_only", &"LineEdit").duplicate()
stylebox_hp.set_border_width_all(1)
stylebox_hp.border_color = Color8(32, 32, 32, 128)
if is_instance_valid(column_add):
column_add.get_child(0).get_child(0).texture = get_theme_icon(&"Add", &"EditorIcons")
column_add.add_theme_stylebox_override(&"normal", stylebox_n)
column_add.add_theme_stylebox_override(&"hover", stylebox_hp)
column_add.add_theme_stylebox_override(&"pressed", stylebox_hp)
if is_instance_valid(warning_sign):
warning_sign.icon = get_theme_icon(&"NodeWarning", &"EditorIcons")
func update() -> void:
if not board_data.layout.changed.is_connected(update):
board_data.layout.changed.connect(update)
var too_high = false
for column in board_data.layout.columns:
if len(column) > 3:
too_high = true
warning_sign.visible = too_high or len(board_data.layout.columns) > 4
for child in column_holder.get_children():
child.queue_free()
var index = 0
for column in board_data.layout.columns:
var column_entry := VBoxContainer.new()
column_entry.add_theme_constant_override(&"separation", 5)
column_holder.add_child(column_entry)
for stage in column:
var stage_entry := Button.new()
stage_entry.tooltip_text = board_data.get_stage(stage).title
stage_entry.focus_mode = Control.FOCUS_NONE
stage_entry.set_v_size_flags(SIZE_EXPAND_FILL)
stage_entry.custom_minimum_size = Vector2i(70, 50)
stage_entry.add_theme_stylebox_override(&"normal", stylebox_n)
stage_entry.add_theme_stylebox_override(&"hover", stylebox_hp)
stage_entry.add_theme_stylebox_override(&"pressed", stylebox_hp)
stage_entry.add_theme_stylebox_override(&"disabled", stylebox_hp)
stage_entry.pressed.connect(__on_remove_stage.bind(stage))
stage_entry.disabled = len(board_data.layout.columns) <= 1 and len(board_data.layout.columns[0]) <= 1
column_entry.add_child(stage_entry)
var center_container := CenterContainer.new()
center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
center_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
stage_entry.add_child(center_container)
var remove := TextureRect.new()
remove.mouse_filter = Control.MOUSE_FILTER_IGNORE
remove.texture = get_theme_icon(&"Remove", &"EditorIcons")
center_container.add_child(remove)
var add = Button.new()
add.custom_minimum_size = Vector2i(70, 40)
add.focus_mode = Control.FOCUS_NONE
add.pressed.connect(__on_add_stage.bind(index))
add.add_theme_stylebox_override(&"normal", stylebox_n)
add.add_theme_stylebox_override(&"hover", stylebox_hp)
add.add_theme_stylebox_override(&"pressed", stylebox_hp)
column_entry.add_child(add)
var center_container = CenterContainer.new()
center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
center_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
add.add_child(center_container)
var plus := TextureRect.new()
plus.mouse_filter = Control.MOUSE_FILTER_IGNORE
center_container.add_child(plus)
plus.texture = get_theme_icon(&"Add", &"EditorIcons")
index += 1
func __on_add_stage(column: int) -> void:
var data = __StageData.new("New Stage")
var uuid = board_data.add_stage(data, true)
var columns = board_data.layout.columns
if column < len(board_data.layout.columns) and column >= 0:
columns[column].append(uuid)
else:
columns.append(PackedStringArray([uuid]))
board_data.layout.columns = columns
func __on_remove_stage(uuid: String) -> void:
if len(board_data.get_stage(uuid).tasks) == 0:
if warn_about_empty_deletion.button_pressed:
if confirm_empty.confirmed.is_connected(__remove_stage):
confirm_empty.confirmed.disconnect(__remove_stage)
confirm_empty.confirmed.connect(__remove_stage.bind(uuid))
confirm_empty.popup_centered()
else:
__remove_stage(uuid)
else:
__update_task_destination(uuid)
if confirm_not_empty.confirmed.is_connected(__remove_stage):
confirm_not_empty.confirmed.disconnect(__remove_stage)
confirm_not_empty.confirmed.connect(__remove_stage.bind(uuid))
confirm_not_empty.popup_centered()
func __update_task_destination(uuid: String) -> void:
task_destination.clear()
for stage in board_data.get_stages():
if stage != uuid:
task_destination.add_item(board_data.get_stage(stage).title)
task_destination.set_item_metadata(-1, stage)
func __remove_stage(uuid: String) -> void:
if len(board_data.get_stage(uuid).tasks) > 0:
var old_tasks = board_data.get_stage(uuid).tasks
var new_tasks = board_data.get_stage(task_destination.get_selected_metadata()).tasks
for task in old_tasks.duplicate():
old_tasks.erase(task)
new_tasks.append(task)
board_data.get_stage(uuid).tasks = old_tasks
board_data.get_stage(task_destination.get_selected_metadata()).tasks = new_tasks
board_data.remove_stage(uuid, true)
var columns = board_data.layout.columns
for column in columns.duplicate():
if uuid in column:
column.remove_at(column.find(uuid))
if len(column) == 0:
columns.erase(column)
board_data.layout.columns = columns
func __settings_changed() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
warn_about_empty_deletion.button_pressed = ctx.settings.warn_about_empty_deletion
func __apply_settings_changes(warn: bool) -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.changed.disconnect(__settings_changed)
ctx.settings.warn_about_empty_deletion = warn
ctx.settings.changed.connect(__settings_changed)

View File

@ -0,0 +1,144 @@
[gd_scene load_steps=5 format=3 uid="uid://dapkpnkm8sow8"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/settings/stages/stages.gd" id="1_1yycq"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7cqkc"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 5.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="Image" id="Image_16p4g"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_frc8x"]
image = SubResource("Image_16p4g")
[node name="Stages" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_1yycq")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
text = "Edit Stage Layout:"
[node name="WarnAboutEmptyDeletion" type="CheckBox" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
text = "Warn about empty deletion."
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_7cqkc")
[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="CenterContainer" type="CenterContainer" parent="PanelContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Grid" type="HBoxContainer" parent="PanelContainer/ScrollContainer/CenterContainer"]
layout_mode = 2
theme_override_constants/separation = 5
[node name="ColumnHolder" type="HBoxContainer" parent="PanelContainer/ScrollContainer/CenterContainer/Grid"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 5
[node name="AddColumn" type="VBoxContainer" parent="PanelContainer/ScrollContainer/CenterContainer/Grid"]
layout_mode = 2
theme_override_constants/separation = 5
[node name="AddColumn" type="Button" parent="PanelContainer/ScrollContainer/CenterContainer/Grid/AddColumn"]
unique_name_in_owner = true
custom_minimum_size = Vector2(40, 105)
layout_mode = 2
size_flags_vertical = 3
focus_mode = 0
[node name="Empty" type="Button" parent="PanelContainer/ScrollContainer/CenterContainer/Grid/AddColumn"]
self_modulate = Color(1, 1, 1, 0)
custom_minimum_size = Vector2(40, 40)
layout_mode = 2
text = "+"
[node name="Warning" type="Control" parent="PanelContainer"]
visible = false
layout_mode = 2
mouse_filter = 2
[node name="WarningSign" type="Button" parent="PanelContainer/Warning"]
unique_name_in_owner = true
layout_mode = 0
anchor_left = 1.0
anchor_right = 1.0
grow_horizontal = 0
tooltip_text = "Adding to much stages can overflow the editor.
Recommended maximum: 4*3"
focus_mode = 0
icon = SubResource("ImageTexture_frc8x")
flat = true
[node name="ConfirmNotEmpty" type="ConfirmationDialog" parent="."]
unique_name_in_owner = true
title = "Delete Stage"
size = Vector2i(403, 159)
[node name="VBoxContainer" type="VBoxContainer" parent="ConfirmNotEmpty"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="ConfirmNotEmpty/VBoxContainer"]
layout_mode = 2
text = "You are deleting a stage which has tasks assigned.
The tasks will be assigned to the following stage:"
[node name="TaskDestination" type="OptionButton" parent="ConfirmNotEmpty/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="ConfirmEmpty" type="ConfirmationDialog" parent="."]
unique_name_in_owner = true
title = "Delete Stage"
size = Vector2i(316, 100)
dialog_text = "Do you really want to delete this stage?
You can not undo this."

View File

@ -0,0 +1,120 @@
@tool
extends Node
var delete := Shortcut.new()
var duplicate := Shortcut.new()
var create := Shortcut.new()
var rename := Shortcut.new()
var search := Shortcut.new()
var confirm := Shortcut.new()
var undo := Shortcut.new()
var redo := Shortcut.new()
var save := Shortcut.new()
var save_as := Shortcut.new()
## Returns whether a specific node should handle the shortcut.
static func should_handle_shortcut(node: Node) -> bool:
var focus_owner := node.get_viewport().gui_get_focus_owner()
return focus_owner and (node.is_ancestor_of(focus_owner) or focus_owner == node)
func _ready() -> void:
__setup_shortcuts()
func __setup_shortcuts() -> void:
# delete
var ev_delete := InputEventKey.new()
if OS.get_name() == "macOS":
ev_delete.keycode = KEY_BACKSPACE
ev_delete.meta_pressed = true
else:
ev_delete.keycode = KEY_DELETE
delete.events.append(ev_delete)
# duplicate
var ev_dupe := InputEventKey.new()
if OS.get_name() == "macOS":
ev_dupe.keycode = KEY_D
ev_dupe.meta_pressed = true
else:
ev_dupe.keycode = KEY_D
ev_dupe.ctrl_pressed = true
duplicate.events.append(ev_dupe)
# create
var ev_create := InputEventKey.new()
if OS.get_name() == "macOS":
ev_create.keycode = KEY_A
ev_create.meta_pressed = true
else:
ev_create.keycode = KEY_A
ev_create.ctrl_pressed = true
create.events.append(ev_create)
# rename
var ev_rename := InputEventKey.new()
ev_rename.keycode = KEY_F2
rename.events.append(ev_rename)
# search
var ev_search := InputEventKey.new()
if OS.get_name() == "macOS":
ev_search.keycode = KEY_F
ev_search.meta_pressed = true
else:
ev_search.keycode = KEY_F
ev_search.ctrl_pressed = true
search.events.append(ev_search)
# confirm
var ev_confirm := InputEventKey.new()
ev_confirm.keycode = KEY_ENTER
confirm.events.append(ev_confirm)
# undo
var ev_undo := InputEventKey.new()
if OS.get_name() == "macOS":
ev_undo.keycode = KEY_Z
ev_undo.meta_pressed = true
else:
ev_undo.keycode = KEY_Z
ev_undo.ctrl_pressed = true
undo.events.append(ev_undo)
# redo
var ev_redo := InputEventKey.new()
if OS.get_name() == "macOS":
ev_redo.keycode = KEY_Z
ev_redo.meta_pressed = true
ev_redo.shift_pressed = true
else:
ev_redo.keycode = KEY_Z
ev_redo.ctrl_pressed = true
ev_redo.shift_pressed = true
redo.events.append(ev_redo)
# save
var ev_save := InputEventKey.new()
if OS.get_name() == "macOS":
ev_save.keycode = KEY_S
ev_save.meta_pressed = true
else:
ev_save.keycode = KEY_S
ev_save.ctrl_pressed = true
save.events.append(ev_save)
# save as
var ev_save_as := InputEventKey.new()
if OS.get_name() == "macOS":
ev_save_as.keycode = KEY_S
ev_save_as.meta_pressed = true
ev_save_as.shift_pressed = true
else:
ev_save_as.keycode = KEY_S
ev_save_as.ctrl_pressed = true
ev_save_as.shift_pressed = true
save_as.events.append(ev_save_as)

View File

@ -0,0 +1,266 @@
@tool
extends MarginContainer
## The visual representation of a stage.
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __Shortcuts := preload("../shortcuts.gd")
const __EditContext := preload("../edit_context.gd")
const __TaskData := preload("../../data/task.gd")
const __TaskScene := preload("../task/task.tscn")
const __TaskScript := preload("../task/task.gd")
const __EditLabel := preload("../../edit_label/edit_label.gd")
const __BoardData := preload("../../data/board.gd")
const __CategoryPopupMenu := preload("../category/category_popup_menu.gd")
var board_data: __BoardData
var data_uuid: String
var __category_menu := __CategoryPopupMenu.new()
@onready var panel_container: PanelContainer = %Panel
@onready var title_label: __EditLabel = %Title
@onready var create_button: Button = %Create
@onready var task_holder: VBoxContainer = %TaskHolder
@onready var scroll_container: ScrollContainer = %ScrollContainer
@onready var preview: Control = %Preview
@onready var preview_color: ColorRect = %Preview/Color
func _ready() -> void:
update()
board_data.get_stage(data_uuid).changed.connect(update.bind(true))
scroll_container.set_drag_forwarding(
_get_drag_data_fw.bind(scroll_container),
_can_drop_data_fw.bind(scroll_container),
_drop_data_fw.bind(scroll_container),
)
create_button.pressed.connect(__on_create_button_pressed)
add_child(__category_menu)
__category_menu.uuid_selected.connect(__on_category_create_popup_uuid_selected)
__category_menu.popup_hide.connect(create_button.set_pressed_no_signal.bind(false))
notification(NOTIFICATION_THEME_CHANGED)
await get_tree().create_timer(0.0).timeout
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if ctx.focus == data_uuid:
ctx.focus = ""
grab_focus()
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
if not Rect2(Vector2(), size).has_point(get_local_mouse_position()):
preview.visible = false
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.create.matches_event(event):
get_viewport().set_input_as_handled()
__category_menu.popup_at_mouse_position(self)
elif shortcuts.rename.matches_event(event):
get_viewport().set_input_as_handled()
title_label.show_edit()
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
preview.visible = true
preview.position.y = __target_height_from_position(at_position)
return data is Dictionary and data.has("task") and data.has("stage")
func _can_drop_data_fw(at_position: Vector2, data: Variant, from: Control) -> bool:
var local_pos = (at_position + from.get_global_rect().position) - get_global_rect().position
return _can_drop_data(local_pos, data)
func _get_drag_data_fw(at_position: Vector2, from: Control) -> Variant:
if from is __TaskScript:
var control := Control.new()
var rect := ColorRect.new()
control.add_child(rect)
rect.size = from.get_rect().size
rect.position = -at_position
rect.color = board_data.get_category(board_data.get_task(from.data_uuid).category).color
from.set_drag_preview(control)
return {
"task": from.data_uuid,
"stage": data_uuid,
}
return null
func _drop_data(at_position: Vector2, data: Variant) -> void:
var index := __target_index_from_position(at_position)
preview.hide()
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.undo_redo.create_action("Move task")
var tasks := board_data.get_stage(data["stage"]).tasks
if data["stage"] == data_uuid:
var old_index := tasks.find(data["task"])
if index < old_index:
tasks.erase(data["task"])
tasks.insert(index, data["task"])
elif index > old_index + 1:
tasks.erase(data["task"])
tasks.insert(index - 1, data["task"])
else:
tasks.erase(data["task"])
ctx.undo_redo.add_do_property(board_data.get_stage(data["stage"]), &"tasks", tasks.duplicate())
ctx.undo_redo.add_undo_property(board_data.get_stage(data["stage"]), &"tasks", board_data.get_stage(data["stage"]).tasks)
tasks = board_data.get_stage(data_uuid).tasks
tasks.insert(index, data["task"])
ctx.focus = data["task"]
ctx.undo_redo.add_do_property(board_data.get_stage(data_uuid), &"tasks", tasks)
ctx.undo_redo.add_undo_property(board_data.get_stage(data_uuid), &"tasks", board_data.get_stage(data_uuid).tasks)
ctx.undo_redo.commit_action()
func _drop_data_fw(at_position: Vector2, data: Variant, from: Control) -> void:
var local_pos = (at_position + from.get_global_rect().position) - get_global_rect().position
_drop_data(local_pos, data)
func _notification(what: int) -> void:
match(what):
NOTIFICATION_THEME_CHANGED:
if is_instance_valid(panel_container):
panel_container.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"panel", &"Tree"))
if is_instance_valid(create_button):
create_button.icon = get_theme_icon(&"Add", &"EditorIcons")
if is_instance_valid(preview_color):
preview_color.color = get_theme_color(&"font_selected_color", &"TabBar")
func update(single: bool = false) -> void:
var focus_owner := get_viewport().gui_get_focus_owner()
if single:
grab_focus()
if title_label.text_changed.is_connected(__set_title):
title_label.text_changed.disconnect(__set_title)
title_label.text = board_data.get_stage(data_uuid).title
title_label.text_changed.connect(__set_title)
var old_scroll := scroll_container.scroll_vertical
if is_instance_valid(focus_owner) and (is_ancestor_of(focus_owner) or focus_owner == self):
if focus_owner is __TaskScript:
__Singletons.instance_of(__EditContext, self).focus = focus_owner.data_uuid
for task in task_holder.get_children():
task.queue_free()
for uuid in board_data.get_stage(data_uuid).tasks:
var task: __TaskScript = __TaskScene.instantiate()
task.board_data = board_data
task.data_uuid = uuid
task.set_drag_forwarding(
_get_drag_data_fw.bind(task),
_can_drop_data_fw.bind(task),
_drop_data_fw.bind(task),
)
task_holder.add_child(task)
scroll_container.scroll_vertical = old_scroll
__update_category_menus()
func __update_category_menus() -> void:
__category_menu.board_data = board_data
func __target_index_from_position(pos: Vector2) -> int:
var global_pos := pos + get_global_position()
if not scroll_container.get_global_rect().has_point(global_pos):
return 0
var scroll_pos := global_pos - task_holder.get_global_position()
var c := 0
for task in task_holder.get_children():
var y = task.position.y + task.size.y/2
if scroll_pos.y < y:
return c
c += 1
return task_holder.get_child_count()
func __set_title(value: String) -> void:
board_data.get_stage(data_uuid).title = value
func __on_create_button_pressed() -> void:
if board_data.get_category_count() > 1:
__category_menu.popup_at_local_position(create_button, Vector2(0, create_button.get_global_rect().size.y))
else:
__create_task(board_data.get_categories()[0])
create_button.set_pressed_no_signal(false)
func __create_task(category: String) -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
var stage_data := board_data.get_stage(data_uuid)
var task_data := __TaskData.new("New task", "", category)
var uuid = board_data.add_task(task_data)
var tasks = stage_data.tasks
tasks.append(uuid)
ctx.undo_redo.create_action("Add task")
ctx.undo_redo.add_do_method(board_data.__add_task.bind(task_data, uuid))
ctx.undo_redo.add_do_property(stage_data, &"tasks", tasks)
ctx.undo_redo.add_undo_property(stage_data, &"tasks", stage_data.tasks)
ctx.undo_redo.add_undo_method(board_data.remove_task.bind(uuid))
ctx.undo_redo.commit_action(false)
stage_data.tasks = tasks
for task in task_holder.get_children():
if task.data_uuid == uuid:
await get_tree().create_timer(0.0).timeout
task.grab_focus()
task.show_edit(__EditLabel.INTENTION.REPLACE)
ctx.filter = null
func __target_height_from_position(pos: Vector2) -> float:
var global_pos = pos + get_global_position()
if not scroll_container.get_global_rect().has_point(global_pos):
return - float(task_holder.get_theme_constant(&"separation")) / 2.0
var scroll_pos: Vector2 = global_pos - task_holder.get_global_position()
var c := 0.0
for task in task_holder.get_children():
var y = task.position.y + task.size.y/2.0
if scroll_pos.y < y:
return c - float(task_holder.get_theme_constant(&"separation")) / 2.0
c += task.size.y + task_holder.get_theme_constant(&"separation")
return c
func __on_category_create_popup_uuid_selected(uuid) -> void:
__create_task(uuid)

View File

@ -0,0 +1,138 @@
[gd_scene load_steps=6 format=3 uid="uid://bjmtdjfx7iqgp"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/stage/stage.gd" id="1_i5556"]
[ext_resource type="Script" path="res://addons/kanban_tasks/edit_label/edit_label.gd" id="2"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h7hiu"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 5.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="Image" id="Image_kabwd"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_yu4gn"]
image = SubResource("Image_kabwd")
[node name="Stage" type="MarginContainer"]
editor_description = "This container is needed because the panel style cannot be updated from a script on the panel container."
custom_minimum_size = Vector2(200, 200)
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 1
theme_override_constants/margin_left = 0
theme_override_constants/margin_top = 0
theme_override_constants/margin_right = 0
theme_override_constants/margin_bottom = 0
script = ExtResource("1_i5556")
[node name="Panel" type="PanelContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_h7hiu")
[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 5
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 0
[node name="Title" type="VBoxContainer" parent="Panel/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 31)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
alignment = 1
script = ExtResource("2")
default_intention = 0
[node name="Create" type="Button" parent="Panel/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Add task."
focus_mode = 0
toggle_mode = true
action_mode = 0
icon = SubResource("ImageTexture_yu4gn")
[node name="HSeparator" type="HSeparator" parent="Panel/VBoxContainer"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 0
[node name="ScrollContainer" type="ScrollContainer" parent="Panel/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
follow_focus = true
horizontal_scroll_mode = 0
[node name="MarginContainer" type="MarginContainer" parent="Panel/VBoxContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="TaskHolder" type="VBoxContainer" parent="Panel/VBoxContainer/ScrollContainer/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
theme_override_constants/separation = 5
[node name="PreviewHolder" type="Control" parent="Panel/VBoxContainer/ScrollContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
mouse_filter = 2
[node name="Preview" type="Control" parent="Panel/VBoxContainer/ScrollContainer/MarginContainer/PreviewHolder"]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 14
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 0
size_flags_vertical = 0
[node name="Color" type="ColorRect" parent="Panel/VBoxContainer/ScrollContainer/MarginContainer/PreviewHolder/Preview"]
custom_minimum_size = Vector2(0, 1)
layout_mode = 1
anchors_preset = 14
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_top = -0.5
offset_bottom = 0.5
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.95, 0.95, 0.95, 1)

View File

@ -0,0 +1,59 @@
@tool
extends Control
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __EditContext := preload("../edit_context.gd")
signal create_board()
signal open_board(path: String)
@onready var create_board_button: LinkButton = %CreateBoard
@onready var open_board_button: LinkButton = %OpenBoard
@onready var recent_board_holder: VBoxContainer = %RecentBoardHolder
@onready var delete_from_recent_dialog: ConfirmationDialog = %DeleteFromRecent
func _ready() -> void:
create_board_button.pressed.connect(func(): create_board.emit())
open_board_button.pressed.connect(func(): open_board.emit(""))
await get_tree().create_timer(0.0).timeout
await get_tree().create_timer(0.0).timeout
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.changed.connect(update)
update()
func update() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
for child in recent_board_holder.get_children():
child.queue_free()
for board in ctx.settings.recent_files:
var button := LinkButton.new()
button.underline = LinkButton.UNDERLINE_MODE_NEVER
button.text = board
button.add_theme_color_override(&"font_color", Color(1, 1, 1, 0.2))
button.pressed.connect(__on_open_recent.bind(board))
recent_board_holder.add_child(button)
func __delete_from_recent(path: String) -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self) as __EditContext
var recent = ctx.settings.recent_files
var i = recent.find(path)
if i >= 0:
recent.remove_at(i)
ctx.settings.recent_files = recent
func __on_open_recent(path: String) -> void:
if not FileAccess.file_exists(path):
if delete_from_recent_dialog.confirmed.is_connected(__delete_from_recent):
delete_from_recent_dialog.confirmed.disconnect(__delete_from_recent)
delete_from_recent_dialog.confirmed.connect(__delete_from_recent.bind(path))
delete_from_recent_dialog.popup_centered()
return
open_board.emit(path)

View File

@ -0,0 +1,103 @@
[gd_scene load_steps=4 format=3 uid="uid://bemcl1rqpeqty"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/start/start.gd" id="1_fkeby"]
[sub_resource type="LabelSettings" id="LabelSettings_0i4nn"]
font_size = 22
[sub_resource type="LabelSettings" id="LabelSettings_5febo"]
font_size = 20
font_color = Color(1, 1, 1, 0.384314)
[node name="Start" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_fkeby")
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer"]
layout_mode = 2
theme_override_constants/separation = 40
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/HBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 11
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/HBoxContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = -10
[node name="Label" type="Label" parent="CenterContainer/HBoxContainer/VBoxContainer/VBoxContainer"]
layout_mode = 2
text = "Kanban Tasks"
label_settings = SubResource("LabelSettings_0i4nn")
[node name="Label2" type="Label" parent="CenterContainer/HBoxContainer/VBoxContainer/VBoxContainer"]
layout_mode = 2
text = "Todo Manager"
label_settings = SubResource("LabelSettings_5febo")
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/HBoxContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 0
theme_override_constants/margin_right = 0
theme_override_constants/margin_bottom = 0
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/HBoxContainer/VBoxContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="CreateBoard" type="LinkButton" parent="CenterContainer/HBoxContainer/VBoxContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Create Board"
underline = 2
[node name="OpenBoard" type="LinkButton" parent="CenterContainer/HBoxContainer/VBoxContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Open Board"
underline = 2
[node name="VSeparator" type="VSeparator" parent="CenterContainer/HBoxContainer"]
layout_mode = 2
[node name="VBoxContainer2" type="VBoxContainer" parent="CenterContainer/HBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="CenterContainer/HBoxContainer/VBoxContainer2"]
layout_mode = 2
text = "Recent Boards"
[node name="MarginContainer2" type="MarginContainer" parent="CenterContainer/HBoxContainer/VBoxContainer2"]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 0
theme_override_constants/margin_right = 0
theme_override_constants/margin_bottom = 0
[node name="RecentBoardHolder" type="VBoxContainer" parent="CenterContainer/HBoxContainer/VBoxContainer2/MarginContainer2"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 0
[node name="DeleteFromRecent" type="ConfirmationDialog" parent="."]
unique_name_in_owner = true
title = "Board not found"
size = Vector2i(388, 106)
ok_button_text = "Delete"
dialog_text = "The board does not seem to exist anymore.
You may choos to remove it from the recent list."
cancel_button_text = "Keep"

View File

@ -0,0 +1,32 @@
@tool
extends Label
@export var auto_size_height: bool = true:
set(value):
auto_size_height = value
queue_redraw()
func _init() -> void:
draw.connect(__before_draw)
func __before_draw() -> void:
# This is needed if wrapping is turned on in an autosized label,
# otherwise the conatiner will give 0 height
# (As the label itself cannot decide what size to ask from the container
# as due to wrapping, no size is fixed. But fortunatelly the label
# makes internal calculation according to intended width before the draw)
if auto_size_height:
var stylebox := get_theme_stylebox(&"normal")
var line_spacing = get_theme_constant(&"line_spacing")
var height := max(0, stylebox.content_margin_top)
for i in get_line_count():
if max_lines_visible >= 0 and i >= max_lines_visible:
break
if i > 0:
height += line_spacing
height += get_line_height(i)
height += max(0, stylebox.content_margin_bottom)
custom_minimum_size.y = height

View File

@ -0,0 +1,439 @@
@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

View File

@ -0,0 +1,108 @@
[gd_scene load_steps=8 format=3 uid="uid://ckqrwj5kxr6vl"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/task/task.gd" id="1_dslv8"]
[ext_resource type="Script" path="res://addons/kanban_tasks/edit_label/edit_label.gd" id="2_iitpi"]
[ext_resource type="Script" path="res://addons/kanban_tasks/view/task/autosize_label.gd" id="3_1qkab"]
[ext_resource type="PackedScene" uid="uid://bwi22eyrmeeet" path="res://addons/kanban_tasks/view/details/details.tscn" id="3_2ol5j"]
[ext_resource type="PackedScene" uid="uid://dwjg5vyxx4g48" path="res://addons/kanban_tasks/view/details/step_holder.tscn" id="4_4e7a7"]
[ext_resource type="Script" path="res://addons/kanban_tasks/expand_button/expand_button.gd" id="5_sgwao"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3iasq"]
bg_color = Color(0.1, 0.1, 0.1, 0.6)
border_width_left = 8
[node name="Task" type="MarginContainer"]
editor_description = "This container is needed because the panel style cannot be updated from a script on the panel container."
custom_minimum_size = Vector2(150, 0)
offset_right = 150.0
offset_bottom = 50.0
size_flags_horizontal = 3
focus_mode = 2
script = ExtResource("1_dslv8")
[node name="Panel" type="PanelContainer" parent="."]
unique_name_in_owner = true
show_behind_parent = true
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_3iasq")
[node name="HBoxContainer" type="HBoxContainer" parent="Panel"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
theme_override_constants/separation = 0
[node name="MarginContainer" type="MarginContainer" parent="Panel/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 0
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/HBoxContainer/MarginContainer"]
layout_mode = 2
alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/HBoxContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="CategoryButton" type="Button" parent="Panel/HBoxContainer/MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
[node name="Title" type="VBoxContainer" parent="Panel/HBoxContainer/MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 34.1)
layout_mode = 2
size_flags_horizontal = 3
alignment = 1
script = ExtResource("2_iitpi")
[node name="Description" type="Label" parent="Panel/HBoxContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
visible = false
modulate = Color(1, 1, 1, 0.443137)
layout_mode = 2
size_flags_horizontal = 3
autowrap_mode = 3
text_overrun_behavior = 3
script = ExtResource("3_1qkab")
[node name="StepHolder" parent="Panel/HBoxContainer/MarginContainer/VBoxContainer" instance=ExtResource("4_4e7a7")]
unique_name_in_owner = true
layout_mode = 2
scrollable = false
steps_can_be_removed = false
steps_can_be_reordered = false
steps_have_context_menu = false
steps_focus_mode = null
[node name="ExpandButton" type="Button" parent="Panel/HBoxContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
theme_type_variation = &"ExpandButton"
flat = true
icon_alignment = 1
script = ExtResource("5_sgwao")
expanded = false
[node name="Edit" type="Button" parent="Panel/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
flat = true
[node name="ContextMenu" type="PopupMenu" parent="."]
unique_name_in_owner = true
allow_search = false
[node name="Details" parent="." instance=ExtResource("3_2ol5j")]
unique_name_in_owner = true

View File

@ -0,0 +1,47 @@
@tool
extends RichTextLabel
@export var mimicked_paragraph_spacing_font_size: int = 6
func _init() -> void:
bbcode_enabled = true
fit_content = true
custom_minimum_size.x = 500
resized.connect(__on_resized)
func _notification(what) -> void:
match what:
NOTIFICATION_ENTER_TREE:
__take_over_label_style()
func mimic_paragraphs() -> void:
var what_in_order: PackedStringArray = [
"[/p]\n[p]",
"[/p][p]",
"[p][/p]",
"[p]",
"[/p]",
]
var forwhat = "\n[font_size=%s]\n[/font_size]\n" % mimicked_paragraph_spacing_font_size
var new_text := text
new_text = new_text.trim_prefix("[p]").trim_suffix("[/p]")
for what in what_in_order:
new_text = new_text.replace(what, forwhat)
new_text = new_text.trim_prefix("\n").trim_suffix("\n")
text = new_text
func __take_over_label_style() -> void:
add_theme_stylebox_override(&"normal", get_theme_stylebox(&"normal", &"Label"))
func __on_resized() -> void:
# Reduce width if unnecessary, as there is no line wraps
var stylebox = get_theme_stylebox(&"normal")
var required_width = get_content_width() + stylebox.content_margin_left + stylebox.content_margin_right
if required_width < custom_minimum_size.x:
custom_minimum_size.x = required_width

40
kanban_tasks_data.kanban Normal file
View File

@ -0,0 +1,40 @@
{
"categories": [
{
"uuid": "a76a310c-9417-4a0b-b02a-64864c7dc07a",
"title": "Task",
"color": "70bafa"
}
],
"stages": [
{
"uuid": "7014ecb9-b7e4-4734-abd1-0897d32d6bc0",
"title": "Todo",
"tasks": []
},
{
"uuid": "386be904-378d-42e9-91c2-6cd2254eb98e",
"title": "Doing",
"tasks": []
},
{
"uuid": "80a2bc87-1518-4944-aeff-68be5c9fc264",
"title": "Done",
"tasks": []
}
],
"tasks": [],
"layout": {
"columns": [
[
"7014ecb9-b7e4-4734-abd1-0897d32d6bc0"
],
[
"386be904-378d-42e9-91c2-6cd2254eb98e"
],
[
"80a2bc87-1518-4944-aeff-68be5c9fc264"
]
]
}
}

View File

@ -21,6 +21,10 @@ window/size/viewport_height=720
window/stretch/mode="canvas_items" window/stretch/mode="canvas_items"
window/handheld/orientation=1 window/handheld/orientation=1
[editor_plugins]
enabled=PackedStringArray("res://addons/kanban_tasks/plugin.cfg")
[rendering] [rendering]
renderer/rendering_method="mobile" renderer/rendering_method="mobile"