Change Specific Fact Tag.fh_lua--[[
@Title: Change Specific Fact Tag
@Type: Standard
@Author: Mark Draper
@Version: 1.0.2
@LastUpdated: 3 Aug 2022
@Licence: This plugin is copyright (c) 2022 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: Converts an Individual or Family Record fact to a new fact type. All facts in fact sets
are supported, including local local project facts.
]]
--[[
30 May 2022 - 1.0
First store version, based on FHUG prototype 0.4
17 Ju1 2022 - 1.0.1
Improved robustness for inconsistent Fact Set structures (various FH bugs)
- List of facts can be anywhere in Fact definition file, not just at beginning
- Does not process facts with no name or label (e.g. Alternative Name in RM Import)
- Ignores missing Fact Sets
- Sets can be in any priority order, not in sequence
3 Aug 2022 - 1.0.2
- Ignores facts missing either index entry or definition to cope with flawed Military Fact Set
- Rewritten Fact Set loading for fewer passes through file
]]
fhInitialise(7,0,0, 'save_recommended') -- only works in FH7
require 'luacom'
require('iuplua')
iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
function main()
local pF = fhNewItemPtr()
-- which record is in the Property Box?
local p = fhGetCurrentPropertyBoxRecord()
if p:IsNull() then
fhMessageBox('Property Box not open!', 'MB_OK', 'MB_ICONSTOP')
return
end
if fhGetTag(p) ~= 'INDI' then
local Msg = 'Plugin requires an Individual Record visible in the Property Box!'
fhMessageBox(Msg, 'MB_OK', 'MB_ICONSTOP')
return
end
-- determine fact set priority and read in sets in decreasing priority order
local tblFactSets = {}
for _, scope in ipairs({'Project', 'System'}) do
GetFactSets(scope, tblFactSets)
end
local tblFacts = {}
for i = #tblFactSets, 1, -1 do
GetFactSet(tblFactSets[i] .. '.fhf', tblFacts)
end
local tblFactGroups = PopulateFactTables(tblFacts)
if not tblFactGroups then return end -- end if duplicate fact labels found
repeat
local tblRecordFacts = GetRecordFacts(p) -- Pointers to the Individual's Facts
local OldFactText, NewFactLabel, OldFactType, NewFactType = DisplayIUP(fhGetDisplayText(p),
tblFacts, tblRecordFacts, tblFactGroups)
if not OldFactText then return end -- Cancel button pressed
-- get pointer to old fact
local pOldFact = tblRecordFacts[OldFactText]
-- get name and tag of new fact
local NewFactName, NewFactTag
for FactTag, FactDetail in pairs(tblFacts) do
if FactDetail.Label == NewFactLabel then
NewFactName = FactDetail.Name
NewFactTag = FactTag:sub(1, -4)
break
end
end
-- check witness roles before confirming change and making substitution
if WitnessCheck(tblFacts, tblRecordFacts[OldFactText], NewFactName) then
local Msg = 'Change ' .. OldFactText .. ' to ' .. NewFactLabel .. '?'
if OldFactType == 'E' and NewFactType == 'A' then
Msg = Msg .. '\n\nCAUTION: You are converting an Event to an Attribute.'
elseif OldFactType == 'A' and NewFactType == 'E' then
Msg = Msg .. '\n\nCAUTION: You are converting an Attribute to an Event.' ..
'\nAny Attribute value will be saved as a local note in the new Event fact.'
end
if fhMessageBox(Msg, 'MB_OKCANCEL', 'MB_ICONQUESTION') == 'OK' then
ChangeFact(pOldFact, NewFactTag)
end
end
until false
end
-- ************************************************************************** --
function ChangeFact(pOldFact, NewFactTag)
-- find fact parent record
local pR = fhNewItemPtr()
pR:MoveToParentItem(pOldFact)
-- add new fact and clone details from old one
local pNewFact = fhCreateItem(NewFactTag, pR)
if pNewFact:IsNull() then
fhMessageBox('Failed to create new fact!', 'MB_OK', 'MB_ICONSTOP')
return
end
fhMoveItemBefore(pNewFact, pOldFact)
fhSetValue_Copy(pNewFact, pOldFact)
CopyChildren(pOldFact, pNewFact) -- recursive copy as per FHUG snippet
-- get any fact value and save as local note if lost
local MsgUpdate = ''
local OldFactValue = fhGetValueAsText(pOldFact) or ''
local NewFactValue = fhGetValueAsText(pNewFact) or ''
if OldFactValue ~= '' and NewFactValue == '' then
if fhGetItemPtr(pOldFact, '~.NOTE2'):IsNotNull() then
MsgUpdate = '\n\nTo view the saved Attribute value, ' ..
'click on the All tab in the FH Property Box.'
end
local pN = fhCreateItem('NOTE2', pNewFact)
fhSetValueAsText(pN, OldFactValue)
end
-- delete old fact
fhDeleteItem(pOldFact)
fhUpdateDisplay()
fhMessageBox('Fact updated.' .. MsgUpdate, 'MB_OK', 'MB_ICONINFORMATION')
end
-- ************************************************************************** --
function CopyBranch(pSource,pTarget)
local Tag = fhGetTag(pSource)
if Tag == '_FMT' then return end -- Skip rich text format code
if Tag == '_FIELD' then -- Substitute metafield shortcut
Tag = fhGetMetafieldShortcut(pSource)
end
local pNew = fhCreateItem(Tag,pTarget,true)
if pNew:IsNull() then return end -- Escape if item not created
fhSetValue_Copy(pNew,pSource)
CopyChildren(pSource,pNew)
end
-- ************************************************************************** --
function CopyChildren(pSource,pTarget)
local pFrom = fhNewItemPtr()
pFrom = pSource:Clone()
pFrom:MoveToFirstChildItem(pFrom)
while pFrom:IsNotNull() do
CopyBranch(pFrom,pTarget)
pFrom:MoveNext()
end
end
-- ************************************************************************** --
function DisplayIUP(strTitle, tblFacts, tblRecordFacts, tblFactGroups)
local OldFactType, OldFactText, NewFactType, NewFactLabel
local Parent, Label
local OK = false
local tblOldEvents, tblOldAttributes = GetOldFacts(tblFacts, tblRecordFacts)
-- set default IUP font
local Registry = 'HKEY_CURRENT_USER\\Software\\Calico Pie\\Family Historian\\2.0\\'
local k = getRegKey(Registry .. 'Preferences\\PDX Font')
local scaling = getRegKey(Registry .. 'Preferences\\Font Scaling Percent') or 100
local tblK = ParseString(k)
local font = tblK[14]
local size = tblK[1] * scaling / 100
iup.SetGlobal('DEFAULTFONT',font .. ' ' .. size//20)
local X = 5 * scaling // 100
local Y = 3 * scaling // 100
local ButtonPadding = (X|0) .. 'x' .. (Y|0)
-- define menu elements
local lstOE = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select existing event'}
for k, v in ipairs(tblOldEvents) do lstOE[k+1] = v end
local OldEvent = iup.hbox{iup.label{title='Event:',size=40}, lstOE}
local lstOA = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select existing attribute'}
for k, v in ipairs(tblOldAttributes) do lstOA[k+1] = v end
local OldAttribute = iup.hbox{iup.label{title='Attribute:',size=40}, lstOA}
local lstNE = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select new event', active='No'}
local NewEvent = iup.hbox{iup.label{title='Event:',size=40}, lstNE}
local lstNA = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select new attribute', active='No'}
local NewAttribute = iup.hbox{iup.label{title='Attribute:',size=40}, lstNA}
local OldFact = iup.vbox{OldEvent, OldAttribute; margin='15x5', gap=5}
local NewFact = iup.vbox{NewEvent, NewAttribute; margin='15x5', gap=5}
local btnOK = iup.button{title='OK', active='No', tip='Process change'}
local btnClear = iup.button{title='Clear', tip='Clear form', padding=ButtonPadding}
local btnClose = iup.button{title='Close', tip='Close plugin'}
local btnHelp = iup.button{title='Help', tip='Show help'}
local Buttons = iup.hbox{iup.fill{}, btnOK, btnClear, btnHelp, btnClose, iup.fill{};
margin='10x10', normalizesize='Both', padding=10, gap=20}
-- define button callbacks
function btnOK:action()
OK = true
return iup.CLOSE
end
function btnClear:action()
lstOE.value = 1
lstOA.value = 1
lstOE.active = 'Yes'
lstOA.active = 'Yes'
ClearNewFacts()
end
function btnClose:action()
return iup.CLOSE
end
function btnHelp:action()
local Cmd = 'https://pluginstore.family-historian.co.uk/page/help/' ..
'change-specific-fact-tag'
fhShellExecute(Cmd)
fhSleep(1000) -- slight pause to suspend immediate redraw
end
-- define list callbacks
function lstOE:valuechanged_cb()
if tonumber(lstOE.value) > 1 then
OldFactText = lstOE[lstOE.value]
Label, Parent = GetOldFactDetails(OldFactText, tblFacts, tblRecordFacts)
PopulateNewFact(Parent, Label)
lstOA.value = 1
lstOA.active = 'No'
OldFactType = 'E'
else
lstOA.active = 'Yes'
if tonumber(lstOA.value) == 1 then -- no old fact now selected
ClearNewFacts()
end
end
end
function lstOA:valuechanged_cb()
if tonumber(lstOA.value) > 1 then
OldFactText = lstOA[lstOA.value]
Label, Parent = GetOldFactDetails(OldFactText, tblFacts, tblRecordFacts)
PopulateNewFact(Parent, Label)
lstOE.value = 1
lstOE.active = 'No'
OldFactType = 'A'
else
lstOE.active = 'Yes'
if tonumber(lstOE.value) == 1 then -- no old fact now selected
ClearNewFacts()
end
end
end
function lstNE:valuechanged_cb()
if tonumber(lstNE.value) > 1 then
NewFactLabel = lstNE[lstNE.value]
NewFactType = 'E'
lstNA.value = 1
lstNA.active = 'No'
btnOK.active = 'Yes'
else
lstNA.active = 'Yes'
btnOK.active = 'No'
end
end
function lstNA:valuechanged_cb()
if tonumber(lstNA.value) > 1 then
NewFactLabel = lstNA[lstNA.value]
NewFactType = 'A'
lstNE.value = 1
lstNE.active = 'No'
btnOK.active = 'Yes'
else
lstNE.active = 'Yes'
btnOK.active = 'No'
end
end
function PopulateNewFact(Parent, Label)
lstNE.RemoveItem = 'All'
lstNA.RemoveItem = 'All'
lstNE.AppendItem = '(none selected)'
lstNA.AppendItem = '(none selected)'
if Parent == 'I' then
for _, v in ipairs(tblFactGroups.IE) do
if v ~= Label then lstNE.AppendItem = v end
end
for _, v in ipairs(tblFactGroups.IA) do
if v ~= Label then lstNA.AppendItem = v end
end
else
for _, v in ipairs(tblFactGroups.FE) do
if v ~= Label then lstNE.AppendItem = v end
end
for _, v in ipairs(tblFactGroups.FA) do
if v ~= Label then lstNA.AppendItem = v end
end
end
lstNE.value = 1
lstNE.active = 'Yes'
lstNA.value = 1
lstNA.active = 'Yes'
btnOK.active = 'No'
end
function ClearNewFacts()
lstNE.value = 1
lstNE.active = 'No'
lstNA.value = 1
lstNA.active = 'No'
btnOK.active = 'No'
end
-- assemble final menu
local Container = iup.vbox{
iup.frame{OldFact; title='Original Event or Attribute:'},
iup.frame{NewFact; title='New Event or Attribute:'},
Buttons; margin='20x20', gap=20}
dlg = iup.dialog{Container, title='Change Fact for ' .. strTitle;
resize='Yes', maxbox='No', minbox='No'}
dlg:map()
local DialogWidth = dlg.NaturalSize:match('^%d+')
local DialogHeight = dlg.NaturalSize:match('%d+$')
dlg.minsize = DialogWidth .. 'x' .. DialogHeight
dlg.maxsize = 'x' .. DialogHeight
dlg:showxy(iup.CENTER,iup.CENTER)
iup.MainLoop()
dlg:destroy()
if OK then return OldFactText, NewFactLabel, OldFactType, NewFactType end
end
-- ************************************************************************** --
function GetFactSet(File, tblFacts)
local tblFile = ReadUTF16File(File)
if not tblFile then return end
-- identify facts listed in index
local tblIndexFacts = {}
local index = false
local tblFactDefs = {}
local ActiveFact, WitnessFact
for _, line in ipairs(tblFile) do
if index then
local FactTag = line:match('^Item%d+=(.+)$')
if FactTag then tblIndexFacts[FactTag] = true end
if line:sub(1,1) == '[' then -- end of index section
index = false
end
elseif line == '[.index]' then
index = true
ActiveFact = nil
end
if line:match('^%[FCT-') and line:match('-[IF][EA]]$') then -- start of fact definition
ActiveFact = line:sub(6, -2)
tblFactDefs[ActiveFact] = {}
end
if ActiveFact then
local term, value = line:match('^(%a+)=(.+)$')
if term == 'Name' then tblFactDefs[ActiveFact].Name = value end
if term == 'Label' then tblFactDefs[ActiveFact].Label = value end
if term == 'Hidden' then tblFactDefs[ActiveFact].Hidden = value end
end
end
-- second pass to identify witness roles (not always listed with main fact definition)
local witnesses
for _, line in ipairs(tblFile) do
if line:match('^%[FCT-') and line:match('-ROLE]$') then -- start of witness definition
WitnessFact = line:sub(6, -7)
end
local _, Role = line:match('^Role(%d+)=(.+)$')
if Role then
if not tblFactDefs[WitnessFact].Witnesses then tblFactDefs[WitnessFact].Witnesses = {} end
table.insert(tblFactDefs[WitnessFact].Witnesses, Role)
end
end
-- add valid facts to main table
for Tag, Fact in pairs(tblFactDefs) do
if not (Fact.Hidden and Fact.Hidden == 'Y') then
if Fact.Name and Fact.Label and tblIndexFacts[Tag] then
tblFacts[Tag] = {}
tblFacts[Tag].Name = Fact.Name
tblFacts[Tag].Label = Fact.Label
if Fact.Witnesses then
tblFacts[Tag].Witnesses = Fact.Witnesses
end
end
end
end
end
-- ************************************************************************** --
function GetFactSets(Context, tblFactSets)
-- read in fact set priority list and store in ordered table
local File, Folder
local tblFS = {}
local MaxValue = 0
if Context == 'Project' then
Folder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
File = Folder .. '\\Fact Types\\GroupIndex.fhdata'
else
Folder = fhGetContextInfo('CI_APP_DATA_FOLDER')
File = Folder .. '\\Fact Types\\Standard\\GroupIndex.fhdata'
end
local tblFile = ReadUTF16File(File)
for _, v in ipairs(tblFile) do
local set, priority = v:match('^(.+)=(%d+)$')
if set and priority then
priority = tonumber(priority)|0
if priority > MaxValue then MaxValue = priority end
if Context == 'System' then
if set == 'Standard' then
tblFS[priority] = Folder .. '\\Fact Types\\Standard\\' .. set
else
tblFS[priority] = Folder .. '\\Fact Types\\Custom\\' .. set
end
else
tblFS[priority] = Folder .. '\\Fact Types\\' .. set
end
end
end
-- copy to main table
for i=1, MaxValue do
if tblFS[i] then table.insert(tblFactSets, tblFS[i]) end
end
end
-- ************************************************************************** --
function GetOldFactDetails(OldFactText, tblFacts, tblRecordFacts)
-- return Parent (I or F) and Label of specified old fact
local pF = tblRecordFacts[OldFactText]
local pR = fhNewItemPtr()
pR:MoveToParentItem(pF)
local Parent = fhGetTag(pR):sub(1, 1)
local Tag = fhGetTag(pF)
if fhIsEvent(pF) then
Tag = Tag .. '-' .. Parent .. 'E'
else
Tag = Tag .. '-' .. Parent .. 'A'
end
if tblFacts[Tag] then
return tblFacts[Tag].Label, Parent
else
return '', Parent -- custom fact does not have a Label
end
end
-- ************************************************************************** --
function GetOldFacts(tblFacts, tblRecordFacts)
-- get fact details
local pF = fhNewItemPtr()
local tblE = {}
local tblA = {}
for RecordText, pF in pairs(tblRecordFacts) do
local FactLabel = GetOldFactDetails(RecordText, tblFacts, tblRecordFacts)
if FactLabel == '' then FactLabel = fhGetDisplayText(pF):match('^%S+') end
local DayNumber = fhCallBuiltInFunction('Daynumber', fhGetValueAsDate(fhGetItemPtr(pF, '~.DATE')):GetDatePt1()) or ''
local SortTag = FactLabel .. DayNumber .. '<>'
if fhIsEvent(pF) then table.insert(tblE, SortTag .. fhGetDisplayText(pF)) end
if fhIsAttribute(pF) then table.insert(tblA, SortTag .. fhGetDisplayText(pF)) end
end
table.sort(tblE) -- alphabetical/chronological
table.sort(tblA)
-- strip out sort tags prior to display
for k, v in ipairs(tblE) do tblE[k] = v:sub(v:find('<>') + 2) end
for k, v in ipairs(tblA) do tblA[k] = v:sub(v:find('<>') + 2) end
return tblE, tblA
end
-- ************************************************************************** --
function GetRecordFacts(p)
-- get Individual and Family Facts for subject individual
local pF = fhNewItemPtr()
local tblT = {}
pF:MoveToFirstChildItem(p)
while pF:IsNotNull() do
if fhGetTag(pF) == 'FAMS' then
local pL = fhGetValueAsLink(pF)
local pFL = fhNewItemPtr()
pFL:MoveToFirstChildItem(pL)
while pFL:IsNotNull() do
if fhIsFact(pFL) then tblT[fhGetDisplayText(pFL)] = pFL:Clone() end
pFL:MoveNext()
end
elseif fhIsFact(pF) then
tblT[fhGetDisplayText(pF)] = pF:Clone()
end
pF:MoveNext()
end
return tblT
end
-- ************************************************************************** --
function PopulateFactTables(tblFacts)
-- check for duplicate fact labels
local tblFactLabels = {}
for _, FactDetails in pairs(tblFacts) do
if tblFactLabels[FactDetails.Label] then
local Msg = 'There is more than one Fact labelled "' .. FactDetails.Label ..
'". Please check your Fact definitions'
fhMessageBox(Msg, 'MB_OK', 'MB_ICONSTOP')
return
end
tblFactLabels[FactDetails.Label] = true
end
-- populate tables used in interface dialogue
local tblIE = {}
local tblIA = {}
local tblFE = {}
local tblFA = {}
local tblFactGroups = {}
for Tag, FactDetails in pairs(tblFacts) do
if Tag:match('IE$') then table.insert(tblIE, FactDetails.Label) end
if Tag:match('IA$') then table.insert(tblIA, FactDetails.Label) end
if Tag:match('FE$') then table.insert(tblFE, FactDetails.Label) end
if Tag:match('FA$') then table.insert(tblFA, FactDetails.Label) end
end
table.sort(tblIE)
table.sort(tblIA)
table.sort(tblFE)
table.sort(tblFA)
tblFactGroups.IE = tblIE
tblFactGroups.IA = tblIA
tblFactGroups.FE = tblFE
tblFactGroups.FA = tblFA
return tblFactGroups
end
-- ************************************************************************** --
function ReadUTF16File(File)
local FileContents = fhLoadTextFile(File, 'UTF-16LE')
if not FileContents then return end
local tblFile = {}
for line in FileContents:gmatch('[^\r\n]+') do
table.insert(tblFile, line)
end
return tblFile
end
-- ************************************************************************** --
function WitnessCheck(tblFacts, pF, NewFactName)
local pS = fhNewItemPtr()
local tblMissing = {}
local AllFound = true
-- identify witnesses and check if role exists in new fact definition
pS:MoveToFirstChildItem(pF)
while pS:IsNotNull() do
if fhGetTag(pS) == '_SHAR' or fhGetTag(pS) == '_SHAN' then
local OldFactRole = fhGetValueAsText(fhGetItemPtr(pS, '~.ROLE'))
local Found = false
for k,v in pairs(tblFacts) do
if k == NewFactName then
if v.Witnesses then
for _, NewFactRole in ipairs(v.Witnesses) do
if NewFactRole == OldFactRole then Found = true end
end
end
end
end
if not Found then
tblMissing[OldFactRole] = true
AllFound = false
end
end
pS:MoveNext()
end
if AllFound then return true end -- no missing witness roles
local Msg = 'The following Witness Roles are present in the old Fact Record but ' ..
'missing from the definition of the new Fact Record. Please update the new ' ..
'Fact definition before changing this fact name. \n\n'
for Role, _ in pairs(tblMissing) do Msg = Msg .. Role .. '\n' end
fhMessageBox(Msg,'MB_OK', 'MB_ICONSTOP')
end
-- ************************************************************************** --
function getRegKey(key)
local sh = luacom.CreateObject 'WScript.Shell'
local ans
if pcall(function () ans = sh:RegRead(key) end) then
return ans
else
return nil,true
end
end
-- *********************************************************************
function ParseString(S)
-- splits a delimited string into a table without using stringx library
local tblT = {}
while true do
local i = S:find(',')
if not i then -- no more delimiters
table.insert(tblT, S)
break
end
table.insert(tblT, S:sub(1,i-1))
S = S:sub(i+1)
end
return tblT
end
-- *********************************************************************
main()
--[[
@Title: Change Specific Fact Tag
@Type: Standard
@Author: Mark Draper
@Version: 1.0.2
@LastUpdated: 3 Aug 2022
@Licence: This plugin is copyright (c) 2022 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: Converts an Individual or Family Record fact to a new fact type. All facts in fact sets
are supported, including local local project facts.
]]
--[[
30 May 2022 - 1.0
First store version, based on FHUG prototype 0.4
17 Ju1 2022 - 1.0.1
Improved robustness for inconsistent Fact Set structures (various FH bugs)
- List of facts can be anywhere in Fact definition file, not just at beginning
- Does not process facts with no name or label (e.g. Alternative Name in RM Import)
- Ignores missing Fact Sets
- Sets can be in any priority order, not in sequence
3 Aug 2022 - 1.0.2
- Ignores facts missing either index entry or definition to cope with flawed Military Fact Set
- Rewritten Fact Set loading for fewer passes through file
]]
fhInitialise(7,0,0, 'save_recommended') -- only works in FH7
require 'luacom'
require('iuplua')
iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
function main()
local pF = fhNewItemPtr()
-- which record is in the Property Box?
local p = fhGetCurrentPropertyBoxRecord()
if p:IsNull() then
fhMessageBox('Property Box not open!', 'MB_OK', 'MB_ICONSTOP')
return
end
if fhGetTag(p) ~= 'INDI' then
local Msg = 'Plugin requires an Individual Record visible in the Property Box!'
fhMessageBox(Msg, 'MB_OK', 'MB_ICONSTOP')
return
end
-- determine fact set priority and read in sets in decreasing priority order
local tblFactSets = {}
for _, scope in ipairs({'Project', 'System'}) do
GetFactSets(scope, tblFactSets)
end
local tblFacts = {}
for i = #tblFactSets, 1, -1 do
GetFactSet(tblFactSets[i] .. '.fhf', tblFacts)
end
local tblFactGroups = PopulateFactTables(tblFacts)
if not tblFactGroups then return end -- end if duplicate fact labels found
repeat
local tblRecordFacts = GetRecordFacts(p) -- Pointers to the Individual's Facts
local OldFactText, NewFactLabel, OldFactType, NewFactType = DisplayIUP(fhGetDisplayText(p),
tblFacts, tblRecordFacts, tblFactGroups)
if not OldFactText then return end -- Cancel button pressed
-- get pointer to old fact
local pOldFact = tblRecordFacts[OldFactText]
-- get name and tag of new fact
local NewFactName, NewFactTag
for FactTag, FactDetail in pairs(tblFacts) do
if FactDetail.Label == NewFactLabel then
NewFactName = FactDetail.Name
NewFactTag = FactTag:sub(1, -4)
break
end
end
-- check witness roles before confirming change and making substitution
if WitnessCheck(tblFacts, tblRecordFacts[OldFactText], NewFactName) then
local Msg = 'Change ' .. OldFactText .. ' to ' .. NewFactLabel .. '?'
if OldFactType == 'E' and NewFactType == 'A' then
Msg = Msg .. '\n\nCAUTION: You are converting an Event to an Attribute.'
elseif OldFactType == 'A' and NewFactType == 'E' then
Msg = Msg .. '\n\nCAUTION: You are converting an Attribute to an Event.' ..
'\nAny Attribute value will be saved as a local note in the new Event fact.'
end
if fhMessageBox(Msg, 'MB_OKCANCEL', 'MB_ICONQUESTION') == 'OK' then
ChangeFact(pOldFact, NewFactTag)
end
end
until false
end
-- ************************************************************************** --
function ChangeFact(pOldFact, NewFactTag)
-- find fact parent record
local pR = fhNewItemPtr()
pR:MoveToParentItem(pOldFact)
-- add new fact and clone details from old one
local pNewFact = fhCreateItem(NewFactTag, pR)
if pNewFact:IsNull() then
fhMessageBox('Failed to create new fact!', 'MB_OK', 'MB_ICONSTOP')
return
end
fhMoveItemBefore(pNewFact, pOldFact)
fhSetValue_Copy(pNewFact, pOldFact)
CopyChildren(pOldFact, pNewFact) -- recursive copy as per FHUG snippet
-- get any fact value and save as local note if lost
local MsgUpdate = ''
local OldFactValue = fhGetValueAsText(pOldFact) or ''
local NewFactValue = fhGetValueAsText(pNewFact) or ''
if OldFactValue ~= '' and NewFactValue == '' then
if fhGetItemPtr(pOldFact, '~.NOTE2'):IsNotNull() then
MsgUpdate = '\n\nTo view the saved Attribute value, ' ..
'click on the All tab in the FH Property Box.'
end
local pN = fhCreateItem('NOTE2', pNewFact)
fhSetValueAsText(pN, OldFactValue)
end
-- delete old fact
fhDeleteItem(pOldFact)
fhUpdateDisplay()
fhMessageBox('Fact updated.' .. MsgUpdate, 'MB_OK', 'MB_ICONINFORMATION')
end
-- ************************************************************************** --
function CopyBranch(pSource,pTarget)
local Tag = fhGetTag(pSource)
if Tag == '_FMT' then return end -- Skip rich text format code
if Tag == '_FIELD' then -- Substitute metafield shortcut
Tag = fhGetMetafieldShortcut(pSource)
end
local pNew = fhCreateItem(Tag,pTarget,true)
if pNew:IsNull() then return end -- Escape if item not created
fhSetValue_Copy(pNew,pSource)
CopyChildren(pSource,pNew)
end
-- ************************************************************************** --
function CopyChildren(pSource,pTarget)
local pFrom = fhNewItemPtr()
pFrom = pSource:Clone()
pFrom:MoveToFirstChildItem(pFrom)
while pFrom:IsNotNull() do
CopyBranch(pFrom,pTarget)
pFrom:MoveNext()
end
end
-- ************************************************************************** --
function DisplayIUP(strTitle, tblFacts, tblRecordFacts, tblFactGroups)
local OldFactType, OldFactText, NewFactType, NewFactLabel
local Parent, Label
local OK = false
local tblOldEvents, tblOldAttributes = GetOldFacts(tblFacts, tblRecordFacts)
-- set default IUP font
local Registry = 'HKEY_CURRENT_USER\\Software\\Calico Pie\\Family Historian\\2.0\\'
local k = getRegKey(Registry .. 'Preferences\\PDX Font')
local scaling = getRegKey(Registry .. 'Preferences\\Font Scaling Percent') or 100
local tblK = ParseString(k)
local font = tblK[14]
local size = tblK[1] * scaling / 100
iup.SetGlobal('DEFAULTFONT',font .. ' ' .. size//20)
local X = 5 * scaling // 100
local Y = 3 * scaling // 100
local ButtonPadding = (X|0) .. 'x' .. (Y|0)
-- define menu elements
local lstOE = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select existing event'}
for k, v in ipairs(tblOldEvents) do lstOE[k+1] = v end
local OldEvent = iup.hbox{iup.label{title='Event:',size=40}, lstOE}
local lstOA = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select existing attribute'}
for k, v in ipairs(tblOldAttributes) do lstOA[k+1] = v end
local OldAttribute = iup.hbox{iup.label{title='Attribute:',size=40}, lstOA}
local lstNE = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select new event', active='No'}
local NewEvent = iup.hbox{iup.label{title='Event:',size=40}, lstNE}
local lstNA = iup.list{'(none selected)', dropdown='Yes', size=200, value=1, expand='Yes',
VisibleItems=10, tip='Select new attribute', active='No'}
local NewAttribute = iup.hbox{iup.label{title='Attribute:',size=40}, lstNA}
local OldFact = iup.vbox{OldEvent, OldAttribute; margin='15x5', gap=5}
local NewFact = iup.vbox{NewEvent, NewAttribute; margin='15x5', gap=5}
local btnOK = iup.button{title='OK', active='No', tip='Process change'}
local btnClear = iup.button{title='Clear', tip='Clear form', padding=ButtonPadding}
local btnClose = iup.button{title='Close', tip='Close plugin'}
local btnHelp = iup.button{title='Help', tip='Show help'}
local Buttons = iup.hbox{iup.fill{}, btnOK, btnClear, btnHelp, btnClose, iup.fill{};
margin='10x10', normalizesize='Both', padding=10, gap=20}
-- define button callbacks
function btnOK:action()
OK = true
return iup.CLOSE
end
function btnClear:action()
lstOE.value = 1
lstOA.value = 1
lstOE.active = 'Yes'
lstOA.active = 'Yes'
ClearNewFacts()
end
function btnClose:action()
return iup.CLOSE
end
function btnHelp:action()
local Cmd = 'https://pluginstore.family-historian.co.uk/page/help/' ..
'change-specific-fact-tag'
fhShellExecute(Cmd)
fhSleep(1000) -- slight pause to suspend immediate redraw
end
-- define list callbacks
function lstOE:valuechanged_cb()
if tonumber(lstOE.value) > 1 then
OldFactText = lstOE[lstOE.value]
Label, Parent = GetOldFactDetails(OldFactText, tblFacts, tblRecordFacts)
PopulateNewFact(Parent, Label)
lstOA.value = 1
lstOA.active = 'No'
OldFactType = 'E'
else
lstOA.active = 'Yes'
if tonumber(lstOA.value) == 1 then -- no old fact now selected
ClearNewFacts()
end
end
end
function lstOA:valuechanged_cb()
if tonumber(lstOA.value) > 1 then
OldFactText = lstOA[lstOA.value]
Label, Parent = GetOldFactDetails(OldFactText, tblFacts, tblRecordFacts)
PopulateNewFact(Parent, Label)
lstOE.value = 1
lstOE.active = 'No'
OldFactType = 'A'
else
lstOE.active = 'Yes'
if tonumber(lstOE.value) == 1 then -- no old fact now selected
ClearNewFacts()
end
end
end
function lstNE:valuechanged_cb()
if tonumber(lstNE.value) > 1 then
NewFactLabel = lstNE[lstNE.value]
NewFactType = 'E'
lstNA.value = 1
lstNA.active = 'No'
btnOK.active = 'Yes'
else
lstNA.active = 'Yes'
btnOK.active = 'No'
end
end
function lstNA:valuechanged_cb()
if tonumber(lstNA.value) > 1 then
NewFactLabel = lstNA[lstNA.value]
NewFactType = 'A'
lstNE.value = 1
lstNE.active = 'No'
btnOK.active = 'Yes'
else
lstNE.active = 'Yes'
btnOK.active = 'No'
end
end
function PopulateNewFact(Parent, Label)
lstNE.RemoveItem = 'All'
lstNA.RemoveItem = 'All'
lstNE.AppendItem = '(none selected)'
lstNA.AppendItem = '(none selected)'
if Parent == 'I' then
for _, v in ipairs(tblFactGroups.IE) do
if v ~= Label then lstNE.AppendItem = v end
end
for _, v in ipairs(tblFactGroups.IA) do
if v ~= Label then lstNA.AppendItem = v end
end
else
for _, v in ipairs(tblFactGroups.FE) do
if v ~= Label then lstNE.AppendItem = v end
end
for _, v in ipairs(tblFactGroups.FA) do
if v ~= Label then lstNA.AppendItem = v end
end
end
lstNE.value = 1
lstNE.active = 'Yes'
lstNA.value = 1
lstNA.active = 'Yes'
btnOK.active = 'No'
end
function ClearNewFacts()
lstNE.value = 1
lstNE.active = 'No'
lstNA.value = 1
lstNA.active = 'No'
btnOK.active = 'No'
end
-- assemble final menu
local Container = iup.vbox{
iup.frame{OldFact; title='Original Event or Attribute:'},
iup.frame{NewFact; title='New Event or Attribute:'},
Buttons; margin='20x20', gap=20}
dlg = iup.dialog{Container, title='Change Fact for ' .. strTitle;
resize='Yes', maxbox='No', minbox='No'}
dlg:map()
local DialogWidth = dlg.NaturalSize:match('^%d+')
local DialogHeight = dlg.NaturalSize:match('%d+$')
dlg.minsize = DialogWidth .. 'x' .. DialogHeight
dlg.maxsize = 'x' .. DialogHeight
dlg:showxy(iup.CENTER,iup.CENTER)
iup.MainLoop()
dlg:destroy()
if OK then return OldFactText, NewFactLabel, OldFactType, NewFactType end
end
-- ************************************************************************** --
function GetFactSet(File, tblFacts)
local tblFile = ReadUTF16File(File)
if not tblFile then return end
-- identify facts listed in index
local tblIndexFacts = {}
local index = false
local tblFactDefs = {}
local ActiveFact, WitnessFact
for _, line in ipairs(tblFile) do
if index then
local FactTag = line:match('^Item%d+=(.+)$')
if FactTag then tblIndexFacts[FactTag] = true end
if line:sub(1,1) == '[' then -- end of index section
index = false
end
elseif line == '[.index]' then
index = true
ActiveFact = nil
end
if line:match('^%[FCT-') and line:match('-[IF][EA]]$') then -- start of fact definition
ActiveFact = line:sub(6, -2)
tblFactDefs[ActiveFact] = {}
end
if ActiveFact then
local term, value = line:match('^(%a+)=(.+)$')
if term == 'Name' then tblFactDefs[ActiveFact].Name = value end
if term == 'Label' then tblFactDefs[ActiveFact].Label = value end
if term == 'Hidden' then tblFactDefs[ActiveFact].Hidden = value end
end
end
-- second pass to identify witness roles (not always listed with main fact definition)
local witnesses
for _, line in ipairs(tblFile) do
if line:match('^%[FCT-') and line:match('-ROLE]$') then -- start of witness definition
WitnessFact = line:sub(6, -7)
end
local _, Role = line:match('^Role(%d+)=(.+)$')
if Role then
if not tblFactDefs[WitnessFact].Witnesses then tblFactDefs[WitnessFact].Witnesses = {} end
table.insert(tblFactDefs[WitnessFact].Witnesses, Role)
end
end
-- add valid facts to main table
for Tag, Fact in pairs(tblFactDefs) do
if not (Fact.Hidden and Fact.Hidden == 'Y') then
if Fact.Name and Fact.Label and tblIndexFacts[Tag] then
tblFacts[Tag] = {}
tblFacts[Tag].Name = Fact.Name
tblFacts[Tag].Label = Fact.Label
if Fact.Witnesses then
tblFacts[Tag].Witnesses = Fact.Witnesses
end
end
end
end
end
-- ************************************************************************** --
function GetFactSets(Context, tblFactSets)
-- read in fact set priority list and store in ordered table
local File, Folder
local tblFS = {}
local MaxValue = 0
if Context == 'Project' then
Folder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
File = Folder .. '\\Fact Types\\GroupIndex.fhdata'
else
Folder = fhGetContextInfo('CI_APP_DATA_FOLDER')
File = Folder .. '\\Fact Types\\Standard\\GroupIndex.fhdata'
end
local tblFile = ReadUTF16File(File)
for _, v in ipairs(tblFile) do
local set, priority = v:match('^(.+)=(%d+)$')
if set and priority then
priority = tonumber(priority)|0
if priority > MaxValue then MaxValue = priority end
if Context == 'System' then
if set == 'Standard' then
tblFS[priority] = Folder .. '\\Fact Types\\Standard\\' .. set
else
tblFS[priority] = Folder .. '\\Fact Types\\Custom\\' .. set
end
else
tblFS[priority] = Folder .. '\\Fact Types\\' .. set
end
end
end
-- copy to main table
for i=1, MaxValue do
if tblFS[i] then table.insert(tblFactSets, tblFS[i]) end
end
end
-- ************************************************************************** --
function GetOldFactDetails(OldFactText, tblFacts, tblRecordFacts)
-- return Parent (I or F) and Label of specified old fact
local pF = tblRecordFacts[OldFactText]
local pR = fhNewItemPtr()
pR:MoveToParentItem(pF)
local Parent = fhGetTag(pR):sub(1, 1)
local Tag = fhGetTag(pF)
if fhIsEvent(pF) then
Tag = Tag .. '-' .. Parent .. 'E'
else
Tag = Tag .. '-' .. Parent .. 'A'
end
if tblFacts[Tag] then
return tblFacts[Tag].Label, Parent
else
return '', Parent -- custom fact does not have a Label
end
end
-- ************************************************************************** --
function GetOldFacts(tblFacts, tblRecordFacts)
-- get fact details
local pF = fhNewItemPtr()
local tblE = {}
local tblA = {}
for RecordText, pF in pairs(tblRecordFacts) do
local FactLabel = GetOldFactDetails(RecordText, tblFacts, tblRecordFacts)
if FactLabel == '' then FactLabel = fhGetDisplayText(pF):match('^%S+') end
local DayNumber = fhCallBuiltInFunction('Daynumber', fhGetValueAsDate(fhGetItemPtr(pF, '~.DATE')):GetDatePt1()) or ''
local SortTag = FactLabel .. DayNumber .. '<>'
if fhIsEvent(pF) then table.insert(tblE, SortTag .. fhGetDisplayText(pF)) end
if fhIsAttribute(pF) then table.insert(tblA, SortTag .. fhGetDisplayText(pF)) end
end
table.sort(tblE) -- alphabetical/chronological
table.sort(tblA)
-- strip out sort tags prior to display
for k, v in ipairs(tblE) do tblE[k] = v:sub(v:find('<>') + 2) end
for k, v in ipairs(tblA) do tblA[k] = v:sub(v:find('<>') + 2) end
return tblE, tblA
end
-- ************************************************************************** --
function GetRecordFacts(p)
-- get Individual and Family Facts for subject individual
local pF = fhNewItemPtr()
local tblT = {}
pF:MoveToFirstChildItem(p)
while pF:IsNotNull() do
if fhGetTag(pF) == 'FAMS' then
local pL = fhGetValueAsLink(pF)
local pFL = fhNewItemPtr()
pFL:MoveToFirstChildItem(pL)
while pFL:IsNotNull() do
if fhIsFact(pFL) then tblT[fhGetDisplayText(pFL)] = pFL:Clone() end
pFL:MoveNext()
end
elseif fhIsFact(pF) then
tblT[fhGetDisplayText(pF)] = pF:Clone()
end
pF:MoveNext()
end
return tblT
end
-- ************************************************************************** --
function PopulateFactTables(tblFacts)
-- check for duplicate fact labels
local tblFactLabels = {}
for _, FactDetails in pairs(tblFacts) do
if tblFactLabels[FactDetails.Label] then
local Msg = 'There is more than one Fact labelled "' .. FactDetails.Label ..
'". Please check your Fact definitions'
fhMessageBox(Msg, 'MB_OK', 'MB_ICONSTOP')
return
end
tblFactLabels[FactDetails.Label] = true
end
-- populate tables used in interface dialogue
local tblIE = {}
local tblIA = {}
local tblFE = {}
local tblFA = {}
local tblFactGroups = {}
for Tag, FactDetails in pairs(tblFacts) do
if Tag:match('IE$') then table.insert(tblIE, FactDetails.Label) end
if Tag:match('IA$') then table.insert(tblIA, FactDetails.Label) end
if Tag:match('FE$') then table.insert(tblFE, FactDetails.Label) end
if Tag:match('FA$') then table.insert(tblFA, FactDetails.Label) end
end
table.sort(tblIE)
table.sort(tblIA)
table.sort(tblFE)
table.sort(tblFA)
tblFactGroups.IE = tblIE
tblFactGroups.IA = tblIA
tblFactGroups.FE = tblFE
tblFactGroups.FA = tblFA
return tblFactGroups
end
-- ************************************************************************** --
function ReadUTF16File(File)
local FileContents = fhLoadTextFile(File, 'UTF-16LE')
if not FileContents then return end
local tblFile = {}
for line in FileContents:gmatch('[^\r\n]+') do
table.insert(tblFile, line)
end
return tblFile
end
-- ************************************************************************** --
function WitnessCheck(tblFacts, pF, NewFactName)
local pS = fhNewItemPtr()
local tblMissing = {}
local AllFound = true
-- identify witnesses and check if role exists in new fact definition
pS:MoveToFirstChildItem(pF)
while pS:IsNotNull() do
if fhGetTag(pS) == '_SHAR' or fhGetTag(pS) == '_SHAN' then
local OldFactRole = fhGetValueAsText(fhGetItemPtr(pS, '~.ROLE'))
local Found = false
for k,v in pairs(tblFacts) do
if k == NewFactName then
if v.Witnesses then
for _, NewFactRole in ipairs(v.Witnesses) do
if NewFactRole == OldFactRole then Found = true end
end
end
end
end
if not Found then
tblMissing[OldFactRole] = true
AllFound = false
end
end
pS:MoveNext()
end
if AllFound then return true end -- no missing witness roles
local Msg = 'The following Witness Roles are present in the old Fact Record but ' ..
'missing from the definition of the new Fact Record. Please update the new ' ..
'Fact definition before changing this fact name. \n\n'
for Role, _ in pairs(tblMissing) do Msg = Msg .. Role .. '\n' end
fhMessageBox(Msg,'MB_OK', 'MB_ICONSTOP')
end
-- ************************************************************************** --
function getRegKey(key)
local sh = luacom.CreateObject 'WScript.Shell'
local ans
if pcall(function () ans = sh:RegRead(key) end) then
return ans
else
return nil,true
end
end
-- *********************************************************************
function ParseString(S)
-- splits a delimited string into a table without using stringx library
local tblT = {}
while true do
local i = S:find(',')
if not i then -- no more delimiters
table.insert(tblT, S)
break
end
table.insert(tblT, S:sub(1,i-1))
S = S:sub(i+1)
end
return tblT
end
-- *********************************************************************
main()
Source:Change-Specific-Fact-Tag-3.fh_lua