Create Source Template Definitions.fh_lua

--[[
@Title:       Create Source Template Definitions
@Type:        Standard
@Author:      Mark Draper
@Version:     1.2
@LastUpdated: 28 Apr 2025
@Licence:     This plugin is copyright (c) 2025 Mark Draper and is licensed under the MIT License which
 is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Creates a Template Definition for user-defined or imported Source Templates where this does
              not exist already, and saves it to the relevant Family Historian Source Templates collection.
              This enables the definitions to be used in other Projects.
]]
--[[
Version 1.2 (Apr 2025)
	- Multi-monitor support added (dedicated form rather than simple iup alarmm)
]]

fhInitialise(7,0,0, 'save_recommended')
fhu = require('fhUtils')
fhu.setIupDefaults()

function main()

	local pT = fhNewItemPtr()
	pT:MoveToFirstRecord('_SRCT')

	-- check if project is using templates

	if pT:IsNull() then
		MessageBox('This project does not use Templated Sources.', 'OK', 'ERROR')
		return
	end

	-- check for duplicate names

	if CheckDuplicates() then return end

	-- read in available template definitions

	local tblCollections, tblHeaders = Initialize()

	-- check Project templates for missing definitions and links

	local tblMissing = {}
	local tblX = {}
	while pT:IsNotNull() do
		if MissingTemplate(pT, tblCollections) then
			table.insert(tblMissing, pT:Clone())
		end
		if fhCallBuiltInFunction('LinksTo', pT) == 0 then table.insert(tblX, pT:Clone()) end
		pT:MoveNext()
	end

	if #tblMissing == 0 then
		local Msg = 'All validated Source Templates have a corresponding definition.\n\n' ..
				'Select "Tools>Source Template Definitions from the main menu to check that ' ..
				'they are fully synchronized.'
		MessageBox(Msg, 'OK', 'INFORMATION')
		return
	end

	local Msg = '\nThere are ' .. #tblMissing .. ' Source Templates in this Project without a ' ..
			'corresponding Source Template Definition.\n\n'
	if #tblMissing == 1 then Msg = '\nThere is 1 Source Template in this Project without a ' ..
			'corresponding Source Template Definition.\n\n' end
	repeat
		local Option = AlarmForm('Create Source Template Definitions (1.2)', Msg,
				'Create Definitions', 'Help', 'Close')
		if Option == 2 then
			local H = 'https://pluginstore.family-historian.co.uk/help/create-source-template-definitions'
			fhShellExecute(H)		-- keep line length below self-imposed style limit
		elseif Option == 0 or Option == 3 then return end
	until Option == 1

	-- ask for permission to make irreversible changes

	Msg = 'Updating Source Template Definitions cannot be reversed by using Edit > Undo Plugin Updates. ' ..
			'Undoing the changes will require restoring the relevant template definition file(s) from a ' ..
			'back up (see the plugin help for more details on where definition files are stored).\n\n' ..
			'Are you sure that you want to proceed?'
	if MessageBox(Msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end

	-- work through missing template definitions

	local tblUpdated = {}		-- new or updated collections
	for _, Template in ipairs(tblMissing) do
		GenerateTemplate(Template, tblCollections)
		tblUpdated[fhGetItemText(Template, '~.COLL')] = true
	end

	for Collection, _ in pairs(tblUpdated) do
		UpdateDefinitionFile(Collection, tblCollections, tblHeaders)
	end

	local Msg = 'Template update complete.\n\nSelect "Tools>Source Template Definitions..." from the ' ..
			'main menu to check that they are fully synchronized.'

	if #tblX > 0 then Msg = Msg .. '\n\n' .. #tblX .. ' Source Templates have no linked sources. ' ..
			'Do you want to delete these from your project?'
		if MessageBox(Msg, 'YESNO', 'QUESTION', nil, 2) == 1 then
			for _, Template in ipairs(tblX) do fhDeleteItem(Template) end
			fhUpdateDisplay()
		end
	else
		MessageBox(Msg, 'OK', 'INFORMATION')
	end
end

-- ******************************************************************************************************--

function AlarmForm(formtitle, heading, txt1, txt2, txt3)

	-- create form modelled on iup.alarm (full form supports parent in multi-monitor setup)

	local action = 0

	local btn1 = iup.button{title = txt1, padding = '10x3',
			action = function(self) action = 1 return iup.CLOSE end}
	local btn2 = iup.button{title = txt2, padding = '10x3',
			action = function(self) action = 2 return iup.CLOSE end}
	local btn3 = iup.button{title = txt3, padding = '10x3',
			action = function(self) action = 3 return iup.CLOSE end}
	local buttons = iup.hbox{iup.fill{}, btn1, btn2, btn3, iup.fill{};
			margin = '0x10', normalizesize = 'Both', padding = 10, gap = 20}
	local label = iup.label{title = heading}
	local vbox = iup.vbox{label, buttons; margin = '20x20'}
	local dialog = iup.dialog{vbox; title = formtitle, resize = 'No', minbox = 'No'}
	if fhGetAppVersion() > 6 then
		iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
	end
	dialog:popup()

	return action
end

-- ******************************************************************************************************--

function CheckDuplicates()

	-- checks for duplicated Template name

	local tblT = {}

	local pT = fhNewItemPtr()
	pT:MoveToFirstRecord('_SRCT')
	while pT:IsNotNull() do
		local Name = fhGetItemText(pT, '~.COLL') .. fhGetItemText(pT, '~.NAME')
		if tblT[Name] then
			local Msg = 'There are multiple Templates called ' .. fhGetItemText(pT, '~.NAME') .. ' in ' ..
				'the ' .. fhGetItemText(pT, '~.COLL') .. ' Collection.  Please ensure that Templates ' ..
				'have a unique name within their Collection prior to creating the Template Definitions.'
			MessageBox(Msg, 'OK', 'ERROR')
			return true
		end
		tblT[Name] = true
		pT:MoveNext()
	end
end

-- ******************************************************************************************************--

function Initialize()

--[[
Builds up a table of Template Collections.
tblD is a table of Template definitions (Collection name => table of Templates in that Collection).
tblH is a table of header information (Collection name => table of XML lines prior to
		the Template definitions in the Collection file).
This method enforces unique Template names within a given Collection, although FH permits duplicate names.
]]

	local field
	local Path = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\Custom'
	local TemplateName
	local tblD = {}
	local tblH = {}

	local FSO = luacom.CreateObject('Scripting.FileSystemObject')
	local objFolder = FSO:GetFolder(Path)
	for _, File in luacom.pairs(objFolder.Files) do
		if File.Name:match('.fhst$') then
			local Collection = File.Name:sub(1, -6)		-- remove extension
			local FileContents = fhLoadTextFile(File.Path)
			if FileContents then
				local tblC = {}
				local tblT = {}
				local header = true
				for line in FileContents:gmatch('[^\r\n]+') do
					if line == '' then
						tblC[TemplateName] = tblT					-- save completed template
						tblT = {}									-- clear table
					end
				end
				tblD[Collection] = tblC				-- save collection
			end
		end
	end
	return tblD, tblH
end

-- ******************************************************************************************************--

function MissingTemplate(pT, tblCollections)

	local Collection = fhGetItemText(pT, '~.COLL')
	local Name = fhGetItemText(pT, '~.NAME')

	if Name == '' then
		MessageBox('Unnamed Template definitions cannot be exported', 'OK', 'WARNING')
		return
	elseif Collection == '' then
		MessageBox('Source Template Record ' .. Name .. ' does not have a defined ' ..
				'Collection, so cannot be exported.', 'OK', 'WARNING')
		return
	elseif Collection == 'Essentials' or Collection == 'Advanced' then
		return
	elseif tblCollections[Collection] and tblCollections[Collection][Name] then
		return
	else
		return true
	end
end

-- ******************************************************************************************************--

function GenerateTemplate(pT, tblCollections)

	local Collection = fhGetItemText(pT, '~.COLL')
	local Name = fhGetItemText(pT, '~.NAME')

	-- build up new Template definition

	local tblT = {'')

	-- update master collection list

	if not tblCollections[Collection] then tblCollections[Collection] = {} end
	tblCollections[Collection][Name] = tblT
end

-- ******************************************************************************************************--

function WriteXMLline(pParent, Tag, XMLtag)

	local p = fhGetItemPtr(pParent, '~.' .. Tag)

	if p:IsNotNull() then
		local Text = fhGetValueAsText(p)
		Text = Text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
		local XML = '<' .. XMLtag .. '>' .. Text .. ''
		return XML
	else
		local XML = '<' .. XMLtag .. '/>'
		return XML
	end
end

-- ******************************************************************************************************--

function UpdateDefinitionFile(Collection, tblCollections, tblHeaders)

	local Path = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\Custom'
	local File = Path .. '\\' .. Collection .. '.fhst'
	local tblT = {}

	if tblHeaders[Collection] then
		tblT = tblHeaders[Collection]
	else
		tblT = {'', ''}
	end
	for _, Template in pairs(tblCollections[Collection]) do
		for _, Line in ipairs(Template) do
			table.insert(tblT, Line)
		end
	end
	table.insert(tblT, '\n')
	fhSaveTextFile(File, table.concat(tblT, '\n'))
end

-- ******************************************************************************************************--

function MessageBox(Message, Buttons, Icon, Title, Default)

	-- replaces built-in function with custom version containing more options

	local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
			title = Title or 'Create Source Template Definitions (1.2)', buttondefault = Default}
	msgdlg:popup()
	return tonumber(msgdlg.ButtonResponse)
end

-- ******************************************************************************************************--

main()

Source:Create-Source-Template-Definitions-1.fh_lua