--[[ @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()