Assign and Cascade Caste.fh_lua--[[
@Title: Assign and Cascade Caste
@Type: Standard
@Author: Mark Draper
@Version: 1.0
@LastUpdated: 23 Jun 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: Assigns Caste to selected individual and cascades to descendants through the male line.
]]
--[[
This Caste assignment plugin was developed as a new author training and demonstration example in
response to a FHUG request for a plugin that cascades defined Caste according to rules defined in the thread
at https://www.fhug.org.uk/forum-1/viewtopic.php?t=23952
It should be used alongside the existing guide available within Family Historian. Unfortunately, there is
only limited material available online on Lua coding compared with much more popular script languages such as
Python and Microsoft VBA, but it is a relatively simple language with a limited number of rules.
Lua was developed originally for Unix-derived systems (it predates Microsoft and DOS/Windows), but most of
its concepts also apply within Windows. It does however retain case-sensitivity in its core structure,
although this is less strictly enforced in the more advanced building of user interfaces using IUP.
]]
require('iuplua')
iup.SetGlobal('CUSTOMQUITMESSAGE', 'YES')
-- declare and assign initial values to 'global' variables, accessible throughout the plugin
-- prefixing variable name is not required, but is recommended to remind the author that it is a global variable
gbl_changes = 0 -- counter for updated records
gbl_tblX = {} -- refuse bin for deleted caste values
-- define plugin functions (functions must be defined before they can be called)
function main()
-- Lua does not require a 'main' function as the initial and controlling function, but this convention is
-- commonly used by authors who learned their programming using C/C++, as it is a mandatory requirement
-- in these languages.
-- identify currently selected Individual ('local' restricts the variable scope to this function,
-- so it is independent of similarly named variables in other functions)
local tblT = fhGetCurrentRecordSel('INDI')
-- stop if not an individual highlighted
if not tblT or #tblT == 0 then
fhMessageBox('Selected Record is not an Individual', 'MB_OK', 'MB_ICONSTOP')
return
end
-- extract individual from one-member table
local pI = fhNewItemPtr() -- create a 'pointer' to point to the stored field
pI = tblT[1] -- assign the pointer
-- get that individual's caste (note that there is nothing in FH preventing storing multiple
-- values for Caste, but for simplicity here it is assumed that there is only one)
local existing_caste = fhGetItemText(pI, '~.CAST')
local msg -- store dialog message
if existing_caste ~= '' then -- give sex-appropriate warning message
if fhGetItemText(pI, '~.SEX') == 'Male' then
msg = fhGetItemText(pI, '~.NAME') .. ' is already assigned to caste ' .. existing_caste ..
'.\n\nOverwrite this value with new selection and cascade to ' ..
'descendants with no further confirmation?'
else
msg = fhGetItemText(pI, '~.NAME') .. ' is already assigned to caste ' .. existing_caste ..
'.\n\nOverwrite this value with new selection?'
end
if fhMessageBox(msg, 'MB_OKCANCEL', 'MB_ICONEXCLAMATION') ~= 'OK' then
return -- cancel execution
end
end
local new_caste = select_caste(pI) -- select caste from user menu
-- end execution if no caste selected
if not new_caste then return end -- simple condition on one line
-- set and cascade caste
set_caste(pI, new_caste)
if fhGetItemText(pI, '~.SEX') == 'Male' then
cascade_caste(pI, new_caste)
end
-- delete any cleared caste fields
if #gbl_tblX > 0 then
for _, p in ipairs(gbl_tblX) do
fhDeleteItem(p)
end
end
-- display the number of Individuals updated
if gbl_changes == 1 then -- ensure message is grammatically correct
msg = '1 Individual record has been updated.'
else
msg = tostring(gbl_changes) .. ' Individual records have been updated.'
end
fhMessageBox(msg, 'MB_OK', 'MB_ICONINFORMATION')
end
function select_caste(pI)
-- get list of all existing caste values by populating table
local new_caste
local tblC = {}
local p = fhNewItemPtr()
p:MoveToFirstRecord('INDI')
while p:IsNotNull() do
local caste = fhGetItemText(p, '~.CAST')
if caste ~= '' then
tblC[caste] = true
end
p:MoveNext()
end
-- define simple addition menu using IUP
local label = iup.label{title = fhGetItemText(pI, '~.NAME')}
local list = iup.list{dropdown = 'YES', editbox = 'YES', size = 120, sort = 'YES'}
local btnOK = iup.button{title = 'OK',
action = function(self)
print(list.value) -- 'prints' the Caste in the debugger
new_caste = list.value -- (useful during development)
return iup.CLOSE
end
}
local btnCancel = iup.button{title = 'Cancel', padding = '10x3',
action = function(self) return iup.CLOSE end}
local hbox = iup.hbox{btnOK, btnCancel; gap = 10, normalizesize = 'BOTH'}
local vbox = iup.vbox{label, list, hbox; gap = 10, margin = '10x10', alignment = 'ACENTER'}
local dialog = iup.dialog{vbox; minbox='NO', maxbox='NO', title = 'Select and enter Caste'}
if fhGetAppVersion() > 6 then -- multiple monitor compatibility
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
end
dialog:map() -- determine form layout prior to populating drop-down list
-- append existing caste values to menu
for caste, _ in pairs(tblC) do
list.AppendItem = caste
end
dialog:popup() -- display the selection menu and wait for a user response
if new_caste == '' then
local msg = 'Selecting a blank Caste will delete any existing Caste value.\n\n' ..
'Are you sure that you want to do this?'
if fhMessageBox(msg, 'MB_OKCANCEL', 'MB_ICONEXCLAMATION') == 'Cancel' then return end
end
return new_caste
end
function set_caste(pI, caste)
-- assign caste to selected individual, overwriting any existing value
local pC = fhGetItemPtr(pI, '~.CAST') -- pointer to any existing caste
local old_caste = fhGetItemText(pI, '~.CAST')
if pC:IsNull() then -- no Caste recorded
pC = fhCreateItem('CAST', pI) -- create blank CAST field
end
if caste == old_caste then
return -- no change in value
elseif caste ~= '' then
fhSetValueAsText(pC, caste) -- set caste, irrespective of sex
else
table.insert(gbl_tblX, pC:Clone()) -- add a copy of the caste pointer to the refuse bin
end
print(fhGetItemText(pI, '~.NAME')) -- 'prints' the name in the debugger
-- (useful during development)
gbl_changes = gbl_changes + 1 -- increment the change counter
end
function cascade_caste(pI, caste)
-- cascade caste to all children of the passed individual
-- to identify their children, we need to find the family first, as the child is part of the
-- family record, not the individual. An individual can have multiple spouses over their lifetime,
-- so we loop through all recorded spouses.
-- my convention here is to use L in the name of any pointer that is a 'link' to a record, rather
-- than pointing to the record itself
local pFL = fhGetItemPtr(pI, '~.FAMS')
while pFL:IsNotNull() do
local pF = fhGetValueAsLink(pFL)
local pCL = fhGetItemPtr(pF, '~.CHIL') -- 'local' restricts this variable to the while loop
while pCL:IsNotNull() do
local pC = fhGetValueAsLink(pCL)
local status = fhGetItemText(pC, '~.FAMC.PEDI')
if status == '' or status == 'Birth' then
set_caste(pC, caste) -- set caste of that child
if fhGetItemText(pC, '~.SEX') == 'Male' then
cascade_caste(pC, caste) -- recursively assign caste via male line
end
end
pCL:MoveNext('SAME_TAG') -- move to next recorded child of that family
end
pFL:MoveNext('SAME_TAG') -- move to next recorded family
end
end
-- call main() function that controls plugin execution
main()
--[[
@Title: Assign and Cascade Caste
@Type: Standard
@Author: Mark Draper
@Version: 1.0
@LastUpdated: 23 Jun 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: Assigns Caste to selected individual and cascades to descendants through the male line.
]]
--[[
This Caste assignment plugin was developed as a new author training and demonstration example in
response to a FHUG request for a plugin that cascades defined Caste according to rules defined in the thread
at https://www.fhug.org.uk/forum-1/viewtopic.php?t=23952
It should be used alongside the existing guide available within Family Historian. Unfortunately, there is
only limited material available online on Lua coding compared with much more popular script languages such as
Python and Microsoft VBA, but it is a relatively simple language with a limited number of rules.
Lua was developed originally for Unix-derived systems (it predates Microsoft and DOS/Windows), but most of
its concepts also apply within Windows. It does however retain case-sensitivity in its core structure,
although this is less strictly enforced in the more advanced building of user interfaces using IUP.
]]
require('iuplua')
iup.SetGlobal('CUSTOMQUITMESSAGE', 'YES')
-- declare and assign initial values to 'global' variables, accessible throughout the plugin
-- prefixing variable name is not required, but is recommended to remind the author that it is a global variable
gbl_changes = 0 -- counter for updated records
gbl_tblX = {} -- refuse bin for deleted caste values
-- define plugin functions (functions must be defined before they can be called)
function main()
-- Lua does not require a 'main' function as the initial and controlling function, but this convention is
-- commonly used by authors who learned their programming using C/C++, as it is a mandatory requirement
-- in these languages.
-- identify currently selected Individual ('local' restricts the variable scope to this function,
-- so it is independent of similarly named variables in other functions)
local tblT = fhGetCurrentRecordSel('INDI')
-- stop if not an individual highlighted
if not tblT or #tblT == 0 then
fhMessageBox('Selected Record is not an Individual', 'MB_OK', 'MB_ICONSTOP')
return
end
-- extract individual from one-member table
local pI = fhNewItemPtr() -- create a 'pointer' to point to the stored field
pI = tblT[1] -- assign the pointer
-- get that individual's caste (note that there is nothing in FH preventing storing multiple
-- values for Caste, but for simplicity here it is assumed that there is only one)
local existing_caste = fhGetItemText(pI, '~.CAST')
local msg -- store dialog message
if existing_caste ~= '' then -- give sex-appropriate warning message
if fhGetItemText(pI, '~.SEX') == 'Male' then
msg = fhGetItemText(pI, '~.NAME') .. ' is already assigned to caste ' .. existing_caste ..
'.\n\nOverwrite this value with new selection and cascade to ' ..
'descendants with no further confirmation?'
else
msg = fhGetItemText(pI, '~.NAME') .. ' is already assigned to caste ' .. existing_caste ..
'.\n\nOverwrite this value with new selection?'
end
if fhMessageBox(msg, 'MB_OKCANCEL', 'MB_ICONEXCLAMATION') ~= 'OK' then
return -- cancel execution
end
end
local new_caste = select_caste(pI) -- select caste from user menu
-- end execution if no caste selected
if not new_caste then return end -- simple condition on one line
-- set and cascade caste
set_caste(pI, new_caste)
if fhGetItemText(pI, '~.SEX') == 'Male' then
cascade_caste(pI, new_caste)
end
-- delete any cleared caste fields
if #gbl_tblX > 0 then
for _, p in ipairs(gbl_tblX) do
fhDeleteItem(p)
end
end
-- display the number of Individuals updated
if gbl_changes == 1 then -- ensure message is grammatically correct
msg = '1 Individual record has been updated.'
else
msg = tostring(gbl_changes) .. ' Individual records have been updated.'
end
fhMessageBox(msg, 'MB_OK', 'MB_ICONINFORMATION')
end
function select_caste(pI)
-- get list of all existing caste values by populating table
local new_caste
local tblC = {}
local p = fhNewItemPtr()
p:MoveToFirstRecord('INDI')
while p:IsNotNull() do
local caste = fhGetItemText(p, '~.CAST')
if caste ~= '' then
tblC[caste] = true
end
p:MoveNext()
end
-- define simple addition menu using IUP
local label = iup.label{title = fhGetItemText(pI, '~.NAME')}
local list = iup.list{dropdown = 'YES', editbox = 'YES', size = 120, sort = 'YES'}
local btnOK = iup.button{title = 'OK',
action = function(self)
print(list.value) -- 'prints' the Caste in the debugger
new_caste = list.value -- (useful during development)
return iup.CLOSE
end
}
local btnCancel = iup.button{title = 'Cancel', padding = '10x3',
action = function(self) return iup.CLOSE end}
local hbox = iup.hbox{btnOK, btnCancel; gap = 10, normalizesize = 'BOTH'}
local vbox = iup.vbox{label, list, hbox; gap = 10, margin = '10x10', alignment = 'ACENTER'}
local dialog = iup.dialog{vbox; minbox='NO', maxbox='NO', title = 'Select and enter Caste'}
if fhGetAppVersion() > 6 then -- multiple monitor compatibility
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
end
dialog:map() -- determine form layout prior to populating drop-down list
-- append existing caste values to menu
for caste, _ in pairs(tblC) do
list.AppendItem = caste
end
dialog:popup() -- display the selection menu and wait for a user response
if new_caste == '' then
local msg = 'Selecting a blank Caste will delete any existing Caste value.\n\n' ..
'Are you sure that you want to do this?'
if fhMessageBox(msg, 'MB_OKCANCEL', 'MB_ICONEXCLAMATION') == 'Cancel' then return end
end
return new_caste
end
function set_caste(pI, caste)
-- assign caste to selected individual, overwriting any existing value
local pC = fhGetItemPtr(pI, '~.CAST') -- pointer to any existing caste
local old_caste = fhGetItemText(pI, '~.CAST')
if pC:IsNull() then -- no Caste recorded
pC = fhCreateItem('CAST', pI) -- create blank CAST field
end
if caste == old_caste then
return -- no change in value
elseif caste ~= '' then
fhSetValueAsText(pC, caste) -- set caste, irrespective of sex
else
table.insert(gbl_tblX, pC:Clone()) -- add a copy of the caste pointer to the refuse bin
end
print(fhGetItemText(pI, '~.NAME')) -- 'prints' the name in the debugger
-- (useful during development)
gbl_changes = gbl_changes + 1 -- increment the change counter
end
function cascade_caste(pI, caste)
-- cascade caste to all children of the passed individual
-- to identify their children, we need to find the family first, as the child is part of the
-- family record, not the individual. An individual can have multiple spouses over their lifetime,
-- so we loop through all recorded spouses.
-- my convention here is to use L in the name of any pointer that is a 'link' to a record, rather
-- than pointing to the record itself
local pFL = fhGetItemPtr(pI, '~.FAMS')
while pFL:IsNotNull() do
local pF = fhGetValueAsLink(pFL)
local pCL = fhGetItemPtr(pF, '~.CHIL') -- 'local' restricts this variable to the while loop
while pCL:IsNotNull() do
local pC = fhGetValueAsLink(pCL)
local status = fhGetItemText(pC, '~.FAMC.PEDI')
if status == '' or status == 'Birth' then
set_caste(pC, caste) -- set caste of that child
if fhGetItemText(pC, '~.SEX') == 'Male' then
cascade_caste(pC, caste) -- recursively assign caste via male line
end
end
pCL:MoveNext('SAME_TAG') -- move to next recorded child of that family
end
pFL:MoveNext('SAME_TAG') -- move to next recorded family
end
end
-- call main() function that controls plugin execution
main()
Source:Assign-and-Cascade-Caste.fh_lua