cancel build right mouse button

This commit is contained in:
2026-06-21 15:36:27 +02:00
parent 04151ad2b1
commit eefbff0fd9
91 changed files with 5697 additions and 11 deletions

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 @@
uid://csd0qg7nc4p7m

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 @@
uid://bymg657d5rk55

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 @@
uid://dwbcvxus65wau

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 @@
uid://rt5utu17k4q4

View File

@@ -0,0 +1,159 @@
@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 stages_width: int = 200:
set(value):
stages_width = 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,
"stages_width": stages_width,
}
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("stages_width"):
stages_width = json["stages_width"]
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 @@
uid://l6lfl3k3qwxw

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 @@
uid://ciuqm01ng1wfq

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 @@
uid://cvg6hllieqvxj

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 @@
uid://b1j5opcoegc6y