Update editor scripts

This commit is contained in:
Insality 2025-04-19 18:03:50 +03:00
parent 8d2b8c25a0
commit 50e59d9469
19 changed files with 1118 additions and 280 deletions

View File

@ -0,0 +1,239 @@
--- Module for assigning layers to GUI nodes based on textures and fonts
local defold_parser = require("druid.editor_scripts.defold_parser.defold_parser")
local system = require("druid.editor_scripts.defold_parser.system.system")
local M = {}
---Create a backup of a file
---@param file_path string - The path of the file to backup
---@return string|nil - The backup file path, or nil if backup failed
local function create_backup(file_path)
local backup_path = file_path .. ".backup"
print("Creating backup at:", backup_path)
-- Read and write using system module
local content, err_read = system.read_file(file_path)
if not content then
print("Error reading original file for backup:", err_read)
return nil
end
local success, err_write = system.write_file(backup_path, content)
if not success then
print("Error creating backup file:", err_write)
return nil
end
print("Backup created successfully")
return backup_path
end
---Restore from a backup file
---@param backup_path string - The path of the backup file
---@param original_path string - The path to restore to
---@return boolean - True if restore was successful
local function restore_from_backup(backup_path, original_path)
print("Restoring from backup:", backup_path)
-- Read backup file
local content, err = system.read_file(backup_path)
if not content then
print("Error reading backup file:", err)
return false
end
-- Write to original file
local success, err = system.write_file(original_path, content)
if not success then
print("Error restoring from backup:", err)
return false
end
print("Restored successfully from backup")
return true
end
---Remove a backup file
---@param backup_path string - The path of the backup file to remove
local function remove_backup(backup_path)
print("Removing backup file:", backup_path)
local success, err = os.remove(backup_path)
if not success then
print("Warning: Could not remove backup file:", err)
print("You may want to manually remove it:", backup_path)
else
print("Backup file removed successfully")
end
end
---Assign layers to GUI nodes based on textures and fonts
---@param gui_resource string - The GUI resource to process
---@return table - Editor command to reload the resource
function M.assign_layers(gui_resource)
local gui_path = editor.get(gui_resource, "path")
print("Setting up layers for", gui_path)
-- Get the absolute path to the file
local absolute_project_path = editor.external_file_attributes(".").path
if not absolute_project_path:match("[\\/]$") then
absolute_project_path = absolute_project_path .. "/"
end
local clean_gui_path = gui_path
if clean_gui_path:sub(1, 1) == "/" then
clean_gui_path = clean_gui_path:sub(2)
end
local gui_absolute_path = absolute_project_path .. clean_gui_path
-- Create a backup before modifying the file
local backup_path = create_backup(gui_absolute_path)
if not backup_path then
print("Failed to create backup, aborting...")
return {}
end
-- Parse the GUI file using defold_parser
print("Parsing GUI file...")
local gui_data = defold_parser.load_from_file(gui_absolute_path)
if not gui_data then
print("Error: Failed to parse GUI file")
return {}
end
-- Collect all textures and fonts
print("Collecting all available textures and fonts...")
local all_textures = {}
local all_fonts = {}
-- Get textures
if gui_data.textures then
for _, texture in ipairs(gui_data.textures) do
print("Found texture:", texture.name)
all_textures[texture.name] = true
end
end
-- Get fonts
if gui_data.fonts then
for _, font in ipairs(gui_data.fonts) do
print("Found font:", font.name)
all_fonts[font.name] = true
end
end
-- Track which textures and fonts are actually used by nodes
print("Finding used textures and fonts...")
local used_layers = {}
-- First pass: find all used textures and fonts
if gui_data.nodes then
for _, node in ipairs(gui_data.nodes) do
if node.texture then
local layer_name = node.texture:match("([^/]+)")
if layer_name and all_textures[layer_name] then
used_layers[layer_name] = true
print("Node", node.id, "uses texture:", layer_name)
end
elseif node.font then
local layer_name = node.font
if all_fonts[layer_name] then
used_layers[layer_name] = true
print("Node", node.id, "uses font:", layer_name)
end
end
end
end
-- Create a set of existing layer names for faster lookup
print("Checking existing layers...")
local existing_layers = {}
if gui_data.layers then
for _, layer in ipairs(gui_data.layers) do
if layer.name then
existing_layers[layer.name] = true
print("Found existing layer:", layer.name)
end
end
end
-- Convert set to array of used layers
local layers = {}
for layer_name in pairs(used_layers) do
if not existing_layers[layer_name] then
table.insert(layers, layer_name)
print("Adding new layer:", layer_name)
else
print("Layer already exists:", layer_name)
end
end
-- Sort new layers for consistent output
table.sort(layers)
print("Found", #layers, "new layers to add")
-- Add new layers (preserving existing ones)
print("Adding new layers...")
gui_data.layers = gui_data.layers or {}
for _, layer_name in ipairs(layers) do
table.insert(gui_data.layers, {
name = layer_name,
})
end
-- Create a lookup table for faster matching - include both existing and new layers
local layer_lookup = {}
for layer_name in pairs(existing_layers) do
layer_lookup[layer_name] = true
end
for _, layer_name in ipairs(layers) do
layer_lookup[layer_name] = true
end
-- Update nodes to use the correct layer
print("Updating node layers...")
if gui_data.nodes then
for _, node in ipairs(gui_data.nodes) do
if node.texture then
local layer_name = node.texture:match("([^/]+)")
if layer_name and layer_lookup[layer_name] then
print("Assigning node", node.id, "to layer:", layer_name)
node.layer = layer_name
end
elseif node.font then
local layer_name = node.font
if layer_lookup[layer_name] then
print("Assigning node", node.id, "to layer:", layer_name)
node.layer = layer_name
end
end
end
end
-- Write the updated GUI file
print("Writing updated GUI file...")
local success = defold_parser.save_to_file(gui_absolute_path, gui_data)
if not success then
print("Error: Failed to save GUI file")
print("Attempting to restore from backup...")
local restored = restore_from_backup(backup_path, gui_absolute_path)
if not restored then
print("Critical: Failed to restore from backup. Manual intervention may be required.")
end
return {}
end
-- Everything worked, remove the backup
remove_backup(backup_path)
print("Successfully assigned layers for GUI:", gui_path)
return {}
end
return M

View File

@ -1,132 +0,0 @@
import os
import sys
import deftree
current_filepath = os.path.abspath(os.path.dirname(__file__))
TEMPLATE_PATH = current_filepath + "/widget.lua_template"
component_annotations = ""
component_functions = ""
component_define = ""
def to_camel_case(snake_str):
components = snake_str.split('_')
return ''.join(x.title() for x in components[0:])
def get_id(node_name):
return node_name.upper().replace("/", "_")
def process_component(node_name, component_name):
global component_annotations
global component_functions
global component_define
if node_name == "root":
component_annotations += "\n---@field root node"
component_define += "\n\tself.root = self:get_node(\"root\")"
if node_name.startswith("button"):
component_annotations += "\n---@field {0} druid.button".format(node_name)
component_functions += "\nfunction M:_on_{0}()\n\tprint(\"Click on {0}\")\nend\n\n".format(node_name)
component_define += "\n\tself.{0} = self.druid:new_button(\"{1}\", self._on_{0})".format(node_name, node_name)
if node_name.startswith("text"):
component_annotations += "\n---@field {0} druid.text".format(node_name)
component_define += "\n\tself.{0} = self.druid:new_text(\"{1}\")".format(node_name, node_name)
if node_name.startswith("lang_text"):
component_annotations += "\n---@field {0} druid.text".format(node_name)
component_define += "\n\tself.{0} = self.druid:new_lang_text(\"{1}\", \"lang_id\")".format(node_name, node_name)
if node_name.startswith("grid") or node_name.startswith("static_grid"):
component_annotations += "\n---@field {0} druid.grid".format(node_name)
component_define += "\n--TODO: Replace prefab_name with grid element prefab"
component_define += "\n\tself.{0} = self.druid:new_grid(\"{1}\", \"prefab_name\", 1)".format(node_name, node_name)
if node_name.startswith("scroll_view"):
field_name = node_name.replace("_view", "")
content_name = node_name.replace("_view", "_content")
component_annotations += "\n---@field {0} druid.scroll".format(field_name)
component_define += "\n\tself.{0} = self.druid:new_scroll(\"{1}\", \"{2}\")".format(field_name, node_name, content_name)
if node_name.startswith("blocker"):
component_annotations += "\n---@field {0} druid.blocker".format(node_name)
component_define += "\n\tself.{0} = self.druid:new_blocker(\"{1}\")".format(node_name, node_name)
if node_name.startswith("slider"):
component_annotations += "\n---@field {0} druid.slider".format(node_name)
component_define += "\n--TODO: Replace slider end position. It should be only vertical or horizontal"
component_define += "\n\tself.{0} = self.druid:new_slider(\"{1}\", vmath.vector3(100, 0, 0), self._on_{0}_change)".format(node_name, node_name)
component_functions += "\nfunction M:_on_{0}_change(value)\n\tprint(\"Slider change:\", value)\nend\n\n".format(node_name)
if node_name.startswith("progress"):
component_annotations += "\n---@field {0} druid.progress".format(node_name)
component_define += "\n\tself.{0} = self.druid:new_progress(\"{1}\", \"x\")".format(node_name, get_id(node_name))
if node_name.startswith("timer"):
component_annotations += "\n---@field {0} druid.timer".format(node_name)
component_define += "\n\tself.{0} = self.druid:new_timer(\"{1}\", 59, 0, self._on_{0}_end)".format(node_name, get_id(node_name))
component_functions += "\nfunction M:_on_{0}_end()\n\tprint(\"Timer {0} trigger\")\nend\n\n".format(node_name)
def main():
global component_annotations
global component_functions
global component_define
filename = sys.argv[1]
print("Create Druid component from gui file", filename)
tree = deftree.parse(filename)
root = tree.get_root()
output_directory = os.path.dirname(filename)
output_filename = os.path.splitext(os.path.basename(filename))[0]
output_full_path = os.path.join(output_directory, output_filename + ".lua")
is_already_exists = os.path.exists(output_full_path)
if is_already_exists:
print("Error: The file is already exists")
print("File:", output_full_path)
return
component_require_path = os.path.join(output_directory, output_filename).replace("/", ".").replace("..", "")
component_name = to_camel_case(output_filename)
component_type = output_filename
scheme_list = []
# Gather nodes from GUI scene
for node in root.iter_elements("nodes"):
node_name = node.get_attribute("id").value
scheme_list.append("\t" + get_id(node_name) + " = \"" + node_name + "\"")
is_template = node.get_attribute("template")
is_in_template = "/" in node_name
if not is_template and not is_in_template:
process_component(node_name, component_name)
if len(component_define) > 2:
component_define = "\n" + component_define
template_file = open(TEMPLATE_PATH, "r")
filedata = template_file.read()
template_file.close()
filedata = filedata.replace("{COMPONENT_NAME}", component_name)
filedata = filedata.replace("{COMPONENT_TYPE}", component_type)
filedata = filedata.replace("{COMPONENT_PATH}", component_require_path)
filedata = filedata.replace("{COMPONENT_DEFINE}", component_define)
filedata = filedata.replace("{COMPONENT_FUNCTIONS}", component_functions)
filedata = filedata.replace("{COMPONENT_ANNOTATIONS}", component_annotations)
#filedata = filedata.replace("{SCHEME_LIST}", ",\n".join(scheme_list))
output_file = open(output_full_path, "w")
output_file.write(filedata)
output_file.close()
print("Success: The file is created")
print("File:", output_full_path)
main()

View File

@ -0,0 +1,61 @@
local M = {}
local function to_camel_case(snake_str)
local components = {}
for component in snake_str:gmatch("[^_]+") do
table.insert(components, component:sub(1, 1):upper() .. component:sub(2))
end
return table.concat(components, "")
end
function M.create_druid_widget(opts)
local gui_filepath = editor.get(opts.selection, "path")
local filename = gui_filepath:match("([^/]+)%.gui$")
print("Create Druid widget for", gui_filepath)
local absolute_project_path = editor.external_file_attributes(".").path
local widget_resource_path = gui_filepath:gsub("%.gui$", ".lua")
local new_widget_absolute_path = absolute_project_path .. widget_resource_path
local widget_name = to_camel_case(filename)
local widget_type = filename
-- Check if file already exists
local f = io.open(new_widget_absolute_path, "r")
if f then
f:close()
print("Widget file already exists at " .. new_widget_absolute_path)
print("Creation aborted to prevent overwriting")
return
end
-- Get template path from preferences
local template_path = editor.prefs.get("druid.widget_template_path")
-- Get template content using the path from preferences
local template_content = editor.get(template_path, "text")
if not template_content then
print("Error: Could not load template from", template_path)
print("Check the template path in [Druid] Settings")
return
end
-- Replace template variables
template_content = template_content:gsub("{COMPONENT_NAME}", widget_name)
template_content = template_content:gsub("{COMPONENT_TYPE}", widget_type)
-- Write file
local file, err = io.open(new_widget_absolute_path, "w")
if not file then
print("Error creating widget file:", err)
return
end
file:write(template_content)
file:close()
print("Widget created at " .. widget_resource_path)
end
return M

View File

@ -0,0 +1,153 @@
--- Defold Text Proto format encoder/decoder to lua table
local config = require("druid.editor_scripts.defold_parser.system.config")
local system = require("druid.editor_scripts.defold_parser.system.system")
local M = {}
--- Decode a Defold object from a string
---@param text string
---@return table
function M.decode_defold_object(text)
-- Create a root object, which will contain all the file data
local root = {}
-- Stack to keep track of nested objects. Always insert data to the last object in the stack
local stack = { root }
-- For each line in the text, we go through the following steps:
for raw_line in text:gmatch("[^\r\n]+") do
system.parse_line(raw_line, stack)
end
return root
end
-- Encoding Functions
function M.encode_defold_object(obj, spaces, data_level, extension)
spaces = spaces or 0
data_level = data_level or 0
local key_order = extension and config.KEY_ORDER[extension] or {}
local result = ''
local tabString = string.rep(' ', spaces)
local keys = {}
for key in pairs(obj) do
table.insert(keys, key)
end
table.sort(keys, function(a, b)
local index_a = system.contains(key_order, a) or 0
local index_b = system.contains(key_order, b) or 0
return index_a < index_b
end)
-- Iterate over the sorted keys
for _, key in ipairs(keys) do
local value = obj[key]
local value_type = type(value)
-- Handle different types of values
if value_type == "table" then
-- Check if it's an array-like table
if #value > 0 then
-- It's an array-like table, process each element
for _, array_item in ipairs(value) do
local item_type = type(array_item)
if key == "data" and item_type == "table" then
-- Handle nested data
local encodedChild = M.encode_defold_object(array_item, spaces + 2, data_level + 1, extension)
result = result .. tabString .. key .. ': "' .. encodedChild .. '"\n'
elseif item_type == "number" or item_type == "boolean" then
local is_contains_dot = string.find(key, "%.")
if item_type == "number" and (system.contains(config.with_dot_params, key) and not is_contains_dot) then
result = result .. tabString .. key .. ': ' .. string.format("%.1f", array_item) .. '\n'
else
result = result .. tabString .. key .. ': ' .. tostring(array_item) .. '\n'
end
elseif item_type == "string" then
-- Handle multiline text
if key == "text" then
result = result .. tabString .. key .. ': "' .. array_item:gsub("\n", '\\n"\n' .. tabString .. '"') .. '"\n'
else
-- Check if the key should not have quotes
local is_uppercase = (array_item == string.upper(array_item))
local is_boolean = (array_item == "true" or array_item == "false")
if (is_uppercase and not config.string_keys[key]) or is_boolean then
result = result .. tabString .. key .. ': ' .. array_item .. '\n'
else
result = result .. tabString .. key .. ': "' .. array_item .. '"\n'
end
end
elseif item_type == "table" then
result = result .. tabString .. key .. ' {\n' .. M.encode_defold_object(array_item, spaces + 2, data_level, extension) .. tabString .. '}\n'
end
end
else
-- It's a dictionary-like table
result = result .. tabString .. key .. ' {\n' .. M.encode_defold_object(value, spaces + 2, data_level, extension) .. tabString .. '}\n'
end
else
-- Handle scalar values (string, number, boolean)
if value_type == "number" or value_type == "boolean" then
local is_contains_dot = string.find(key, "%.")
if value_type == "number" and (system.contains(config.with_dot_params, key) and not is_contains_dot) then
result = result .. tabString .. key .. ': ' .. string.format("%.1f", value) .. '\n'
else
result = result .. tabString .. key .. ': ' .. tostring(value) .. '\n'
end
elseif value_type == "string" then
-- Handle multiline text
if key == "text" then
result = result .. tabString .. key .. ': "' .. value:gsub("\n", '\\n"\n' .. tabString .. '"') .. '"\n'
else
-- Check if the key should not have quotes
local is_uppercase = (value == string.upper(value))
local is_boolean = (value == "true" or value == "false")
if (is_uppercase and not config.string_keys[key]) or is_boolean then
result = result .. tabString .. key .. ': ' .. value .. '\n'
else
result = result .. tabString .. key .. ': "' .. value .. '"\n'
end
end
end
end
end
return result
end
---Load lua table from file in Defold Text Proto format
---@param file_path string
---@return table|nil, string|nil
function M.load_from_file(file_path)
local content, reason = system.read_file(file_path)
if not content then
return nil, reason
end
return M.decode_defold_object(content), nil
end
---Write lua table to file in Defold Text Proto format
---The path file extension will be used to determine the Defold format (*.atlas, *.gui, *.font, etc)
---@param file_path string
---@param lua_table table
---@return boolean, string|nil
function M.save_to_file(file_path, lua_table)
-- Get extension without the dot
local defold_format_name = file_path:match("^.+%.(.+)$")
print("File extension:", defold_format_name)
local encoded_object = M.encode_defold_object(lua_table, nil, nil, defold_format_name)
return system.write_file(file_path, encoded_object)
end
return M

View File

@ -0,0 +1,179 @@
local M = {}
-- Define a set of keys that should not have quotes
M.string_keys = {
text = true,
id = true,
value = true,
rename_patterns = true,
}
M.ALWAYS_LIST = {
attributes = true,
nodes = true,
images = true,
children = true,
fonts = true,
layers = true,
textures = true,
embedded_components = true,
embedded_instances = true,
collection_instances = true,
instances = true,
}
M.with_dot_params = {
"x",
"y",
"z",
"w",
"alpha",
"outline_alpha",
"shadow_alpha",
"text_leading",
"text_tracking",
"pieFillAngle",
"innerRadius",
"leading",
"tracking",
"data",
"t_x",
"t_y",
"spread",
"start_delay",
"inherit_velocity",
"start_delay_spread",
"duration_spread",
"start_offset",
"outline_width",
"shadow_x",
"shadow_y",
"aspect_ratio",
"far_z",
"mass",
"linear_damping",
"angular_damping",
"gain",
"pan",
"speed",
"duration"
}
M.KEY_ORDER = {
["font"] = {
"extrude_borders",
"images",
"inner_padding",
"margin",
"font",
"material",
"size",
"antialias",
"alpha",
"outline_alpha",
"outline_width",
"shadow_alpha",
"shadow_blur",
"shadow_x",
"shadow_y",
"extra_characters",
"output_format",
"all_chars",
"cache_width",
"cache_height",
"render_mode",
},
["atlas"] = {
"id",
"images",
"playback",
"fps",
"flip_horizontal",
"flip_vertical",
"image",
"sprite_trim_mode",
"images",
"animations",
"margin",
"extrude_borders",
"inner_padding",
"max_page_width",
"max_page_height",
"rename_patterns",
},
["gui"] = {
"position",
"rotation",
"scale",
"size",
"color",
"type",
"blend_mode",
"text",
"texture",
"font",
"id",
"xanchor",
"yanchor",
"pivot",
"outline",
"shadow",
"adjust_mode",
"line_break",
"parent",
"layer",
"inherit_alpha",
"slice9",
"outerBounds",
"innerRadius",
"perimeterVertices",
"pieFillAngle",
"clipping_mode",
"clipping_visible",
"clipping_inverted",
"alpha",
"outline_alpha",
"shadow_alpha",
"overridden_fields",
"template",
"template_node_child",
"text_leading",
"text_tracking",
"size_mode",
"spine_scene",
"spine_default_animation",
"spine_skin",
"spine_node_child",
"particlefx",
"custom_type",
"enabled",
"visible",
-- Scene
"scripts",
"fonts",
"textures",
"background_color",
"nodes",
"layers",
"material",
"layouts",
"adjust_reference",
"max_nodes",
"spine_scenes",
"particlefxs",
"resources",
"materials",
"max_dynamic_textures",
-- Vectors
"x",
"y",
"z",
"w",
},
}
return M

View File

@ -0,0 +1,139 @@
local config = require("druid.editor_scripts.defold_parser.system.config")
local M = {}
-- Example: "name: value"
M.REGEX_KEY_COLUM_VALUE = "^%s*([%w_]+):%s*(.+)$"
-- Example: "name {"
M.REGEX_START_TABLE = "^%s*([%w_]*)%s*{%s*$"
-- Example: "}"
M.REGEX_END_TABLE = "^%s*}%s*$"
---@param value string
---@return string
function M.unescape_text_field(value)
-- Splitting the value by new lines and processing each line
local lines = {}
for line in value:gmatch("[^\r\n]+") do
line = line:gsub('\\"', '"') -- Unescaping quotes
line = line:gsub("\\n", "") -- Removing newline escapes
line = line:gsub("\\", "") -- Unescaping backslashes
table.insert(lines, line)
end
-- Reconstructing the value
value = table.concat(lines, "\n")
return value
end
function M.is_multiline_value(value)
return value:find("\\n\"") ~= nil
end
---@param value any
---@param property_name string|nil
---@return any
function M.decode_value(value, property_name)
if value:match('^".*"$') then
-- Removing the quotes from the string
value = value:sub(2, -2)
-- Check if value is escaped
-- If ends with \n
if value:sub(-2) == "\\n" then
value = value:gsub('\\"', '"') -- Unescaping quotes
value = value:gsub("\\n", "")
value = value:gsub("\\", "")
end
elseif value:match('^%-?[0-9.E%-]+$') then
-- Converting to number
value = tonumber(value)
end
-- Specific handling for the "text" property
if property_name == "text" then
value = tostring(value)
else
if value == "true" then
value = true
elseif value == "false" then
value = false
end
end
if property_name == "text" and M.is_multiline_value(value) and type(value) == "string" then
value = M.unescape_text_field(value)
end
return value
end
---@param parent_object table
---@param name string
---@param stack table
function M.new_inner_struct(parent_object, name, stack)
local new_object = {}
M.apply_value(parent_object, name, new_object)
local is_object_always_list = config.ALWAYS_LIST[name]
if is_object_always_list and not M.is_array(parent_object[name]) then
parent_object[name] = { parent_object[name] }
end
table.insert(stack, new_object)
end
---Apply value to the object, if the value is already present, convert it to an array
---@param object table
---@param name string
---@param value any
---@return table object
function M.apply_value(object, name, value)
local is_object_always_list = config.ALWAYS_LIST[name]
if object[name] == nil then
object[name] = value
if is_object_always_list then
object[name] = { object[name] }
end
return object
end
-- Convert to array if not already
if not M.is_array(object[name]) then
object[name] = { object[name] }
end
table.insert(object[name], value)
return object
end
---@param object table
---@param value string
---@return table @object
function M.apply_multiline_value(object, name, value)
if object[name] == nil then
object[name] = value
else
object[name] = object[name] .. "\n" .. value
end
return object
end
--- Check if table is array
---@param t table
---@return boolean
function M.is_array(t)
return type(t) == "table" and t[1] ~= nil
end
return M

View File

@ -0,0 +1,163 @@
local parser = require("druid.editor_scripts.defold_parser.system.parser")
local M = {}
--- Check if table-array contains element
---@param table table
---@param element any
---@return number|boolean index of element or false
function M.contains(table, element)
for index, value in pairs(table) do
if value == element then
return index
end
end
return false
end
---@param file_path string
---@return string|nil, string|nil @success, reason
function M.read_file(file_path)
local file = io.open(file_path, "r")
if file == nil then
return nil, "Could not open file: " .. file_path
end
local content = file:read("*a")
file:close()
return content, nil
end
---@param file_path string
---@param content string
---@return boolean, string|nil @success, reason
function M.write_file(file_path, content)
local file = io.open(file_path, "w")
if file == nil then
return false, "Could not open file: " .. file_path
end
file:write(content)
file:close()
return true, nil
end
---@param line string
function M.unescape_line(line)
-- Trim whitespaces
line = line:match("^%s*(.-)%s*$")
-- Remove first and last quote symbols only if exists
if line:sub(1, 1) == '"' and line:sub(-1) == '"' then
line = line:sub(2, -2)
end
-- Trim whitespaces
line = line:match("^%s*(.-)%s*$")
-- Splitting the value by new lines and processing each line
line = line:gsub('\\"', '"') -- Unescaping quotes
line = line:gsub("\\n", "") -- Removing newline escapes
line = line:gsub("\\", "") -- Unescaping backslashes
return line
end
---@param line string
---@return string, string, string, boolean @new_object_name, name, value, end_struct_flag
function M.split_line(line)
local new_object_name = line:match(parser.REGEX_START_TABLE)
local name, value = line:match(parser.REGEX_KEY_COLUM_VALUE)
local end_struct_flag = line:match(parser.REGEX_END_TABLE)
-- We hit a line what is contains only value, like multiline strings
if not name and not value then
value = line
end
return new_object_name, name, value, end_struct_flag
end
-- what a crap...
local LAST_USED_NAME = nil
---@param unescaped_line string @line to parse
---@param stack table @stack of objects
---@return boolean
function M.parse_line(unescaped_line, stack)
unescaped_line = unescaped_line:match("^%s*(.-)%s*$")
-- Use last object to insert data
local object = stack[#stack]
local line = M.unescape_line(unescaped_line)
local inner_object_name, name, value, end_struct_flag = M.split_line(line)
local is_just_new_line = (unescaped_line == "\"\\n\"")
if not end_struct_flag and (line == "\"" or line == "") and (not is_just_new_line) then
if LAST_USED_NAME ~= "text" then
end_struct_flag = true
end
end
if inner_object_name then
parser.new_inner_struct(object, inner_object_name, stack)
object = stack[#stack]
end
if name and value ~= nil then
-- If value is nested object...
if value:sub(1, 1) == '"' then
value = value:sub(2, -1)
end
if value:sub(-1) == '"' then
value = value:sub(1, -2)
end
local unescape_line = M.unescape_line(value)
local new_object_name, field_name, _, end_flag = M.split_line(unescape_line)
if (new_object_name or field_name or end_flag) and name ~= "text" then
parser.new_inner_struct(object, name, stack)
object = stack[#stack]
M.parse_line(value, stack)
else
-- Just a hack honestly
-- If first character is a quote, then remove it
if value:sub(1, 1) == '"' then
value = value:sub(2, -1)
end
if value:sub(-1) == '"' then
value = value:sub(1, -2)
end
value = parser.decode_value(value, name)
LAST_USED_NAME = name
parser.apply_value(object, name, value)
end
end
if not name and value and not inner_object_name and not end_struct_flag then
-- We should to add value to the last as a multiline data
parser.apply_multiline_value(object, LAST_USED_NAME, value)
end
if end_struct_flag then
-- Go back to the parent object
table.remove(stack)
end
return true
end
return M

View File

@ -1,25 +1,24 @@
local assign_layers = require("druid.editor_scripts.assign_layers")
local create_druid_widget = require("druid.editor_scripts.create_druid_widget")
local druid_settings = require("druid.editor_scripts.druid_settings")
local M = {}
local DEFAULT_WIDGET_TEMPLATE_PATH = "/druid/templates/widget_full.lua.template"
local function ends_with(str, ending)
return ending == "" or str:sub(-#ending) == ending
end
local function save_file_from_dependency(dependency_file_path, output_file_path)
local content = editor.get(dependency_file_path, "text")
local file, err = io.open(output_file_path, "w")
if not file then
print("Error:", err)
return false
end
file:write(content)
file:close()
print("Write file at", output_file_path)
return true
---Define preferences schema
function M.get_prefs_schema()
return {
["druid.widget_template_path"] = editor.prefs.schema.string({
default = DEFAULT_WIDGET_TEMPLATE_PATH,
scope = editor.prefs.SCOPE.PROJECT
})
}
end
---Define the editor commands
function M.get_commands()
return {
{
@ -28,24 +27,10 @@ function M.get_commands()
query = { selection = {type = "resource", cardinality = "one"} },
active = function(opts)
local path = editor.get(opts.selection, "path")
return ends_with(path, ".gui")
return path:match("%.gui$") ~= nil
end,
run = function(opts)
local file = opts.selection
print("Run script for", editor.get(file, "path"))
save_file_from_dependency('/druid/editor_scripts/run_python_script_on_gui.sh', "./build/run_python_script_on_gui.sh")
save_file_from_dependency('/druid/editor_scripts/setup_layers.py', "./build/setup_layers.py")
return {
{
action = "shell",
command = {
"bash",
"./build/run_python_script_on_gui.sh",
"./build/setup_layers.py",
"." .. editor.get(file, "path")
}
}
}
return assign_layers.assign_layers(opts.selection)
end
},
@ -55,25 +40,18 @@ function M.get_commands()
query = { selection = {type = "resource", cardinality = "one"} },
active = function(opts)
local path = editor.get(opts.selection, "path")
return ends_with(path, ".gui")
return path:match("%.gui$") ~= nil
end,
run = function(opts)
local file = opts.selection
print("Run script for", editor.get(file, "path"))
save_file_from_dependency('/druid/editor_scripts/run_python_script_on_gui.sh', "./build/run_python_script_on_gui.sh")
save_file_from_dependency('/druid/editor_scripts/create_druid_component.py', "./build/create_druid_component.py")
save_file_from_dependency('/druid/editor_scripts/widget.lua_template', "./build/widget.lua_template")
return {
{
action = "shell",
command = {
"bash",
"./build/run_python_script_on_gui.sh",
"./build/create_druid_component.py",
"." .. editor.get(file, "path")
}
}
}
return create_druid_widget.create_druid_widget(opts.selection)
end
},
{
label = "[Druid] Settings",
locations = { "Edit" },
run = function()
return druid_settings.open_settings()
end
}
}

View File

@ -0,0 +1,122 @@
local M = {}
function M.open_settings()
print("Opening Druid settings")
local dialog_component = editor.ui.component(function(props)
local template_path, set_template_path = editor.ui.use_state(editor.prefs.get("druid.widget_template_path"))
-- Check if the template path is valid
local path_valid = editor.ui.use_memo(function(path)
-- Use resource_exists to check if the resource exists
local exists = false
pcall(function()
-- If we can get the text property, the resource exists
local content = editor.get(path, "text")
exists = content ~= nil
end)
return exists
end, template_path)
return editor.ui.dialog({
title = "Druid Settings",
content = editor.ui.vertical({
spacing = editor.ui.SPACING.MEDIUM,
padding = editor.ui.PADDING.MEDIUM,
children = {
editor.ui.label({
text = "Widget Template Path:"
}),
editor.ui.resource_field({
value = template_path,
on_value_changed = set_template_path,
extensions = {"lua", "template"},
padding = editor.ui.PADDING.SMALL
}),
not path_valid and editor.ui.label({
text = "Warning: Path not found!",
color = editor.ui.COLOR.WARNING
}) or nil,
-- Links section title
editor.ui.label({
text = "Documentation:",
color = editor.ui.COLOR.TEXT
}),
-- Documentation buttons
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.button({
text = "Project Repository",
on_pressed = function()
editor.browse("https://github.com/Insality/druid")
end
}),
editor.ui.button({
text = "Open Quick API Reference",
on_pressed = function()
editor.browse("https://github.com/Insality/druid/blob/develop/api/quick_api_reference.md")
end
}),
}
}),
-- Sponsor section
editor.ui.label({
text = "Support the project:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.button({
text = "❤️ Sponsor on GitHub",
on_pressed = function()
editor.browse("https://github.com/sponsors/Insality")
end
}),
editor.ui.button({
text = "☕ Ko-fi",
on_pressed = function()
editor.browse("https://ko-fi.com/insality")
end
}),
editor.ui.button({
text = "☕ Buy Me A Coffee",
on_pressed = function()
editor.browse("https://buymeacoffee.com/insality")
end
})
}
})
}
}),
buttons = {
editor.ui.dialog_button({
text = "Cancel",
cancel = true
}),
editor.ui.dialog_button({
text = "Save",
default = true,
result = { template_path = template_path }
})
}
})
end)
local result = editor.ui.show_dialog(dialog_component({}))
if result and result.template_path then
-- Update the preferences
editor.prefs.set("druid.widget_template_path", result.template_path)
print("Widget template path updated to:", result.template_path)
end
return result
end
return M

View File

@ -1,24 +0,0 @@
#!/bin/bash
# @license MIT, Insality 2022
# @source https://github.com/Insality/druid
echo "Run bash for $1"
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# Check if pip3 is installed
if command -v pip3 &> /dev/null; then
PIP_CMD="pip3"
PYTHON_CMD="python3"
else
PIP_CMD="pip"
PYTHON_CMD="python"
fi
is_defree_installed=$($PIP_CMD list --disable-pip-version-check | grep -E "deftree")
if [ -z "$is_defree_installed" ]; then
echo "The python deftree is not installed. Please install it via"
echo "$ $PIP_CMD install deftree"
exit 0
fi
$PYTHON_CMD $1 $2

View File

@ -1,44 +0,0 @@
# @license MIT, Insality 2021
# @source https://github.com/Insality/druid
import sys
import deftree
def main():
filename = sys.argv[1]
print("Auto setup layers for file", filename)
tree = deftree.parse(filename)
root = tree.get_root()
layers = []
for texture in root.iter_elements("textures"):
layers.append(texture.get_attribute("name").value)
for fonts in root.iter_elements("fonts"):
layers.append(fonts.get_attribute("name").value)
to_remove_layers = []
for layer in root.iter_elements("layers"):
to_remove_layers.append(layer)
for layer in to_remove_layers:
root.remove(layer)
for layer in layers:
new_layer = root.add_element("layers")
new_layer.add_attribute("name", layer)
for node in root.iter_elements("nodes"):
texture = node.get_attribute("texture")
font = node.get_attribute("font")
if texture and texture.value:
layer = texture.value.split("/")[0]
node.set_attribute("layer", layer)
if font:
layer = font.value
node.set_attribute("layer", layer)
tree.write()
main()

View File

@ -0,0 +1,9 @@
---@class widget.{COMPONENT_TYPE}: druid.widget
local M = {}
function M:init()
end
return M

View File

@ -13,6 +13,7 @@ function M:init()
-- self.druid:new_button([node_id], [callback])
-- self.druid:new_text([node_id], [text])
-- And all functions from component.lua file
self.root = self:get_node("root")
self.button = self.druid:new_button("button", self.on_button, self)
end
@ -23,4 +24,4 @@ function M:on_button()
end
return M
return M

View File

@ -14,7 +14,7 @@ function M:init()
self.druid:new_hover("slider_back", nil, self.on_slider_back_hover)
for index = 1, 13 do
self.druid:new_button("button" .. index .. "/root", self.on_button_click)
self.druid:new_button("button" .. index .. "/root", self.on_button_click, index)
end
end
@ -30,8 +30,10 @@ function M:on_slider(value)
end
---@param params any
---@param button druid.button
function M:on_button_click(_, button)
function M:on_button_click(params, button)
print("on_button_click", params, button)
local node = button.node
self.scroll:scroll_to(gui.get_position(node))
end

View File

@ -13,8 +13,8 @@ high_dpi = 1
update_frequency = 60
[project]
title = druid
version = 1.0
title = Druid
version = 1.1
publisher = Insality
developer = Maksim Tuprikov
custom_resources = /example/locales

View File

@ -79,7 +79,7 @@ Widgets are reusable UI components that encapsulate multiple **Druid** component
### Creating a Widget
Create a new Lua file for your widget class. This file better to be placed near the corresponding GUI file with the same name.
Create a new Lua file for your widget class. This file better to be placed near the corresponding GUI file with the same name. You can use the Druid's editor script to create a widget by right-clicking on the GUI file in the editor or in "Edit" menu panel, while GUI file is opened.
Define `init` function to initialize the widget.

View File

@ -5,6 +5,8 @@ Custom compomnents from 1.1 release are deprecated. Now we have a new way to cre
Custom components are will exists for more system things like basic components. You don't have to migrate to widgets.
The editor script for creating custom components is removed. Now you can create widgets with the new editor script.
Read more about widgets in [widgets.md](widgets.md)
## Overview
@ -17,7 +19,7 @@ Every component is a child of the Basic Druid component. You can call methods of
### Basic Component Template
A basic custom component template looks like this (you can copy it from `/druid/templates/component.template.lua`):
A basic custom component template looks like this (you can copy it from `/druid/templates/component.lua.template`):
```lua
local component = require("druid.component")
@ -58,7 +60,7 @@ end
### Full Component Template
A full custom component template looks like this (you can copy it from `/druid/templates/component_full.template.lua`):
A full custom component template looks like this (you can copy it from `/druid/templates/component_full.lua.template`):
```lua
local component = require("druid.component")
@ -138,29 +140,6 @@ function init(self)
end
```
## Create Druid Component Editor Script
Druid provides an editor script to assist you in creating Lua files for your GUI scenes. You can find the commands under the menu `Edit -> Create Druid Component` when working with *.gui scenes.
The script analyzes the current GUI scene and generates a Lua file with stubs for all Druid components found. The output file is named after the current GUI scene and placed in the same directory. Note that the script does not override any existing *.lua files. If you want to regenerate a file, delete the previous version first.
The script requires `python` with `deftree` installed. If `deftree` is not installed, the instructions will be displayed in the console.
### Auto-Layout Components
The generator script also checks the current GUI scene for Druid components and creates stubs for them. If a node name starts with a specific keyword, the script generates component stubs in the Lua file. For example, nodes named `button` and `button_exit` will result in the generation of two Druid Button components with callback stubs.
Available keywords:
- `button`: Adds a [Druid Button](01-components.md#button) component and generates the callback stub.
- `text`: Adds a [Druid Text](01-components.md#text) component.
- `lang_text`: Adds a [Druid Lang Text](01-components.md#lang-text) component.
- `grid` or `static_grid`: Adds a [Druid Static Grid](01-components.md#static-grid) component. You should set up the Grid prefab for this component after generating the file.
- `scroll_view`: Adds a [Druid Scroll](01-components.md#scroll) component. It also adds a `scroll_content` node with the same postfix. Ensure that it's the correct node.
- `blocker`: Adds a [Druid Blocker](01-components.md#blocker) component.
- `slider`: Adds a [Druid Slider](01-components.md#slider) component. You should adjust the end position of the Slider after generating the file.
- `progress`: Adds a [Druid Progress](01-components.md#progress) component.
- `timer`: Adds a [Druid Timer](01-components.md#timer) component.
## The Power of Using Templates
With Druid, you can use a single component but create and customize templates for it. Templates only need to match the component scheme. For example, you can have a component named `player_panel` and two GUI templates named `player_panel` and `enemy_panel` with different layouts. The same component script can be used for both templates.

View File

@ -168,3 +168,16 @@ function init(self)
end
```
## Create Druid Widget Editor Script
Druid provides an editor script to assist you in creating Lua files for your GUI scenes. You can find the commands under the menu `Edit -> Create Druid Widget` when working with *.gui scenes.
This script will create a new widget lua file with the same name and basic template for the widget.
The Druid provides two templates:
- `/druid/templates/widget.lua.template` - Basic template for the widget.
- `/druid/templates/widget_full.lua.template` - Full template for the widget.
You can change the path to the template in the `[Druid] Settings` option in the `Edit` menu.