Plugins and Language Packs for Family Historian

Change Any Fact Tag.fh_lua

--[[
@Title:			Change Any Fact Tag
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			4.2
@Keywords:		
@LastUpdated:		12 Nov 2026
@Licence:			This plugin is copyright (c) 2026 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description:		Changes or deletes any Individual Fact or Family Fact or Record Tag.
@V4.2:				Fix for <hidden> CHR,BAPM,ANUL,DIV default events in strButtonTitle(...); Fix <Custom Events/Attributes> fact name filter; (Must test with only one Fact Set defining Standard facts)
@V4.1:				Library 4.1; Fix problem with <hidden> facts; Fix problem with initial Source/Target Tag Set selection; Remove refresh XP tooltip;
@V4.0:				Library 4.0; Centre windows on FH window; Check for Updates button;
@V3.9:				Workaround for wierd bug with undefined custom facts in doLoadUndefinedTags();
@V3.8:				Fix progress bar; Add skip button to Warnings; Lock plugin window while picking records;
@V3.7:				Update to library V3.3; Fix bugs with Project only Fact Sets and duplicate Fact Labels; Support multiple Project only Fact Sets; Support UTF-8 file paths and Fact Labels; DirTree to GetFolderContents;
@V3.6:				Add new FH v7 fields: FamilySearch Id, Unique Id;
@V3.5:				Ensure (project) Fact Set takes precedence over Standard Fact Set;
@V3.4:				fhConvertUTF8toANSI() needed for Project only Fact Set files; CheckVersionInStore() at monthly intervals; Aggressive memory management;
@V3.3:				Handle rich text and metafields; Add SOUR ~ _FIELD & HEAD ~ _PCIT; For Event to Attribute changes move any _UNCAT value to Attribute value;
@V3.2:				Project only Fact Sets; Delete iup_gui.History = " "; Correct tblControls={ … [btnGetHelp] … Plugin Store";
@V3.1:				Updated library to Functions Prototypes v3.0; iup_gui.Balloon = "NO" for PlayOnLinux/Mac; FH V7 Lua 3.5 IUP 3.28; Plugin Store Help;
@V3.0:				Updated code for Fact Defs based on Export Gedcom File. Updated library module for safer IUP GUI. Fix bug with long Fact Name/Label and no Abbreviation.
@V2.9:				Add filter by record selection.
@V2.8:				Bug fix to cope with Fact Set files having no Hidden tag line at all.
@V2.7:				Bug fix in strstrParseFactItem() for Witness Role items, and elsewhere for <hidden> Standard events BAPM, CHR, ANUL, DIV.
@V2.6:				Add _PLACe filter match for EMIG/IMMIgration Events, and cater for GroupIndex.fhdata <eclipsed> and also <hidden> Facts.
@V2.5:				Cope with Custom Fact names same as Standard/Defined names in TblDictionary, Date filter matches DATE:LONG or DATE:COMPACT as in Property Box, StrDisplayData uses BuildDataRef, and latest library modules.
@V2.4:				Both ANSI FH V5 & UTF-8 FH V6 IUP 3.11.2 Unicode settings, File > Encoding=ANSI/UTF-8, iup.SetGlobal("UTF8MODE","YES"), HelpDialogue conditional ExpandChildren="YES/NO", RefreshDialogue NaturalSize,
@V2.4:				Updated GUI library module, better Tab tooltip, refresh toggle & droplist tooltips for XP, add BalloonToggle(), handle UTF-16 FactSet files, UTF-8 Filter values, and V6 Place Records.
@V2.3:				GUI improved tool tips, StrBlue eliminated, background=StrWhite, V5.0.2 fhCreateItem FILE/FORM update, Version in Store check, Version History help, new library modules, minor code revisions.
@V2.2:				Added the Interface Font option, Result Set of changes, Knowledge Base Help, V5.0.0.3/4 fixes, and GUI & code revisions.
@V2.1:				Added the View Log and Purge Log buttons to user interface.
@V2.0:				Tested with many UDF Facts & Tags, and revised user interface with Filter option.
@V1.1:				Copes with most UDF Fact anomalies and adds many minor user interface features.
					Any unconvertible data and error reports are both logged and saved in SOURce Notes.
@V1.0 Beta:		Supports the Standard Fact Set, any Custom Fact Sets, undefined Custom Facts, and UDF Facts.
]]

if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end

--[[
@Title:			aa Library Functions Preamble
@Author:			Mike Tate
@Version:			4.1
@LastUpdated:		18 Feb 2026
@Description:		All the library functions prototype closures for Plugins.
]]

--[[
@Module:			+fh+stringx_v3
@Author:			Mike Tate
@Version:			3.0
@LastUpdated:	19 Sep 2020
@Description:	Extended string functions to supplement LUA string library.
@V3.0:				Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility; Added inert(strTxt) function;
@V2.5:				Support FH V6 Encoding = UTF-8;
@V2.4:				Tolerant of integer & nil parameters just link match & gsub;
@V1.0:				Initial version.
]]

local function stringx_v3()

	local fh = {}									-- Local environment table

	-- Supply current file encoding format --
	function fh.encoding()
		if fhGetAppVersion() > 5 then return fhGetStringEncoding() end
		return "ANSI"
	end -- function encoding

	-- Split a string using "," or chosen separator --
	function fh.split(strTxt,strSep)
		local tblFields = {}
		local strPattern = string.format("([^%s]+)", strSep or ",")
		strTxt = tostring(strTxt or "")
		strTxt:gsub(strPattern, function(strField) tblFields[#tblFields+1] = strField end)
		return tblFields
	end -- function split

	-- Split a string into numbers using " " or "," or "x" separators	-- Any non-number remains as a string
	function fh.splitnumbers(strTxt)
		local tblNum = {}
		strTxt = tostring(strTxt or "")
		strTxt:gsub("([^ ,x]+)", function(strNum) tblNum[#tblNum+1] = tonumber(strNum) or strNum end)
		return tblNum
	end -- function splitnumbers

	local strMagic = "([%^%$%(%)%%%.%[%]%*%+%-%?])"							-- UTF-8 replacement for "(%W)"

	-- Hide magic pattern symbols	^ $ ( ) % . [ ] * + - ?
	function fh.plain(strTxt)
		-- Prefix every magic pattern character with a % escape character,
		-- where %% is the % escape, and %1 is the original character capture.
		strTxt = tostring(strTxt or ""):gsub(strMagic,"%%%1")
		return strTxt
	end -- function plain

	-- matches is plain text version of string.match()
	function fh.matches(strTxt,strFind,intInit)
		strFind = tostring(strFind or ""):gsub(strMagic,"%%%1")			-- Hide magic pattern symbols
		return tostring(strTxt or ""):match(strFind,tonumber(intInit))
	end -- function matches

	-- replace is plain text version of string.gsub()
	function fh.replace(strTxt,strOld,strNew,intNum)
		strOld = tostring(strOld or ""):gsub(strMagic,"%%%1")				-- Hide magic pattern symbols
		return tostring(strTxt or ""):gsub(strOld,function() return strNew end,tonumber(intNum))	-- Hide % capture symbols
	end -- function replace

	-- Hide % escape/capture symbols in replacement so they are inert
	function fh.inert(strTxt)
		strTxt = tostring(strTxt or ""):gsub("%%","%%%%")					-- Hide all % symbols
		return strTxt
	end -- function inert

	-- convert is pattern without captures version of string.gsub()
	function fh.convert(strTxt,strOld,strNew,intNum)
		return tostring(strTxt or ""):gsub(tostring(strOld or ""),function() return strNew end,tonumber(intNum))	-- Hide % capture symbols
	end -- function convert

	local dicUpper = { }
	local dicLower = { }
	local dicCaseX = { }
	-- ASCII unaccented letter translations for Upper, Lower, and Case Insensitive
	for intUpper = string.byte("A"), string.byte("Z") do
		local strUpper = string.char(intUpper)
		local strLower = string.char(intUpper - string.byte("A") + string.byte("a"))
		dicUpper[strLower] = strUpper
		dicLower[strUpper] = strLower
		local strCaseX = "["..strUpper..strLower.."]"
		dicCaseX[strLower] = strCaseX
		dicCaseX[strUpper] = strCaseX
	end

	-- Supply character length of ANSI text --
	function fh.length(strTxt)
		return string.len(strTxt or "")
	end -- function length

	-- Supply character substring of ANSI text --
	function fh.substring(strTxt,i,j)
		return string.sub(strTxt or "",i,j)
	end -- function substring

	-- Translate upper/lower case ANSI letters to pattern that matches both --
	function fh.caseless(strTxt)
		strTxt = tostring(strTxt or ""):gsub("[A-Za-z]",dicCaseX)
		return strTxt
	end -- function caseless

	if fh.encoding() == "UTF-8" then

		-- Supply character length of UTF-8 text --
		function fh.length(strTxt)
			isFlag = fhIsConversionLossFlagSet()
			strTxt = fhConvertUTF8toANSI(strTxt or "")
			fhSetConversionLossFlag(isFlag)
			return string.len(strTxt)
		end -- function length

		local strUTF8 = "([%z\1-\127\194-\244][\128-\191]*)"			-- Cater for Lua 5.1 %z or Lua 5.3 \0
		if fhGetAppVersion() > 6 then
			strUTF8 = "([\0-\127\194-\244][\128-\191]*)"
		end

		-- Supply character substring of UTF-8 text --
		function fh.substring(strTxt,i,j)
			local strSub = ""
			j = j or -1
			if j < 0 then j = j + length(strTxt) + 1 end
			if i < 0 then i = i + length(strTxt) + 1 end
			for strChr in string.gmatch(strTxt or "",strUTF8) do
				if j <= 0 then break end
				j = j - 1
				i = i - 1
				if i <= 0 then strSub = strSub..strChr end
			end
			return strSub
		end -- function substring

		-- Translate lower case to upper case UTF-8 letters --
		function fh.upper(strTxt)
			strTxt = tostring(strTxt or ""):gsub("([a-z\194-\244][\128-\191]*)",dicUpper)
			return strTxt
		end -- function upper

		-- Translate upper case to lower case UTF-8 letters --
		function fh.lower(strTxt)
			strTxt = tostring(strTxt or ""):gsub("([A-Z\194-\244][\128-\191]*)",dicLower)
			return strTxt
		end -- function lower

		-- Translate upper/lower case UTF-8 letters to pattern that matches both --
		function fh.caseless(strTxt)
			strTxt = tostring(strTxt or ""):gsub("([A-Za-z\194-\244][\128-\191]*)",dicCaseX)
			return strTxt
		end -- function caseless

		-- Following tables use ASCII numeric coding to be immune from ANSI/UTF-8 encoding --

		local arrPairs =	-- Upper & Lower case groups of UTF-8 letters with same prefix --
		{--	{ Prefix; Beg ; End ; Inc; Offset Upper > Lower };			-- These include all ANSI letters and many more
			{ "\195"; 0x80; 0x96;  1 ; 32 };								-- 195=0xC3 À U+00C0 to Ö U+00D6 and à U+00E0 to ö U+00F6
			{ "\195"; 0x98; 0x9E;  1 ; 32 };								-- 195=0xC3 Ø U+00D8 to Þ U+00DE and ø U+00F8 to þ U+00FE
			{ "\196"; 0x80; 0xB6;  2 ;  1 };								-- 196=0xC4 A U+0100 to k U+0137 in pairs
			{ "\196"; 0xB9; 0xBD;  2 ;  1 };								-- 196=0xC4 L U+0139 to l U+013E in pairs
			{ "\197"; 0x81; 0x87;  2 ;  1 };								-- 197=0xC5 L U+0141 to n U+0148 in pairs
			{ "\197"; 0x8A; 0xB6;  2 ;  1 };								-- 197=0xC5 ? U+014A to y U+0177 in pairs
			{ "\197"; 0xB9; 0xBD;  2 ;  1 };								-- 197=0xC5 Z U+0179 to ž U+017E in pairs
			{ "\198"; 0x82; 0x84;  2 ;  1 };								-- 198=0xC6 ?  U+0182 to ?  U+0185 in pairs
			-- Add more Unicode groups here as usage increases --
		}
		local dicPairs =	-- Upper v Lower case UTF-8 letters that don't fit groups above --
		{	[string.char(0xC4,0xBF)] = string.char(0xC5,0x80);			-- ? U+013F and ? U+0140
			[string.char(0xC5,0xB8)] = string.char(0xC3,0xBF);			-- Ÿ U+0178 and ÿ U+00FF
		}

		local intBeg1 = string.byte(string.sub("À",1))
		local intBeg2 = string.byte(string.sub("À",2))
		local intEnd1 = string.byte(string.sub("Z",1))
		local intEnd2 = string.byte(string.sub("Z",2))
	--	print(string.format("%#x %#x %#x %#x",intBeg1,intBeg2,intEnd1,intEnd2)) -- Useful to work out numeric coding

		-- Populate the UTF-8 letter translation dictionaries --
		for intGroup, tblGroup in ipairs ( arrPairs ) do				-- UTF-8 accented letter groups
			local strPrefix = tblGroup[1]
			for intUpper = tblGroup[2], tblGroup[3], tblGroup[4] do
				local strUpper = string.char(intUpper)
				local strLower = string.char(intUpper + tblGroup[5])
				local strCaseX = strPrefix.."["..strUpper..strLower.."]"
				strUpper = strPrefix..strUpper
				strLower = strPrefix..strLower
				dicUpper[strLower] = strUpper
				dicLower[strUpper] = strLower
				dicCaseX[strLower] = strCaseX
				dicCaseX[strUpper] = strCaseX
			end
		end
		for strUpper, strLower in pairs ( dicPairs ) do					-- UTF-8 accented letters where upper & lower have different prefix
			dicUpper[strLower] = strUpper
			dicLower[strUpper] = strLower
			local strCaseX = ""
			for intByte = 1, #strUpper do									-- Matches more than just the two letters, but can't do any better
				strCaseX = strCaseX.."["..strUpper:sub(intByte,intByte)..strLower:sub(intByte,intByte).."]"
			end
			dicCaseX[strLower] = strCaseX
			dicCaseX[strUpper] = strCaseX
		end

	end

	-- overload fh functions into string table
	for strIndex, anyValue in pairs(fh) do
		if type(anyValue) == "function" then
			string[strIndex] = anyValue
		end
	end

	return fh

end -- local function stringx_v3

local stringx = stringx_v3()													-- To access FH string extension module

--[[
@Module:			+fh+iterate_v3
@Author:			Mike Tate
@Version:			3.0
@LastUpdated:	25 Aug 2020
@Description:	An iterater functions module to supplement LUA functions.
@V3.0:				Function Prototype Closure version.
@V1.2:				RecordTypes() includes HEAD tag.
@V1.1:				?
@V1.0:				Initial version.
]]

local function iterate_v3()

	local fh = {}																-- Local environment table

	-- Iterator for all records of one chosen type --
	function fh.Records(strType)
		local ptrAll = fhNewItemPtr()										-- Pointer to all records in turn
		local ptrRec = fhNewItemPtr()										-- Pointer to record returned to user
		ptrAll:MoveToFirstRecord(strType)
		return function ()
			ptrRec:MoveTo(ptrAll)
			ptrAll:MoveNext()
			if ptrRec:IsNotNull() then return ptrRec end
		end
	end -- function Records

	-- Iterator for all the record types --
	function fh.RecordTypes()
		local intNext = -1														-- Next record type number
		local intLast = fhGetRecordTypeCount()								-- Last record type number
		return function()
			intNext = intNext + 1
			if intNext == 0 then												-- Includes HEAD tag -- V1.2
				return "HEAD"
			elseif intNext <= intLast then
				return fhGetRecordTypeTag(intNext)							-- Return record type tag
			end
		end
	end -- function RecordTypes

	-- Iterator for all items in all records of chosen types --
	function fh.Items(...)
		local arg = {...}
		local intType = 1														-- Integer record type number
		local tblType = {}														-- Table of record type tags
		local ptrNext = fhNewItemPtr()										-- Pointer to next item in turn
		local ptrItem = fhNewItemPtr()										-- Pointer to item returned to user

		if #arg == 0 then
			for intType = 1, fhGetRecordTypeCount() do					-- No parameters so use all record types
				tblType[intType] = fhGetRecordTypeTag(intType)
			end
		else
			tblType = arg														-- Got parameters so use them instead
		end
	--	print(tblType[intType],intType)
		ptrNext:MoveToFirstRecord(tblType[intType])						-- Get first record of first type

		return function()
			repeat
				while ptrNext:IsNotNull() do									-- Loop through all items
					ptrItem:MoveTo(ptrNext)
					ptrNext:MoveNextSpecial()
					if ptrItem:IsNotNull() then return ptrItem end
				end
				intType = intType + 1											-- Loop through each record type
				if intType <= #tblType then
					ptrNext:MoveToFirstRecord(tblType[intType])
				end
			until intType > #tblType
		end
	end -- function Items

	-- Iterator for all facts of an individual --
	function fh.Facts(ptrIndi)
		local ptrItem = fhNewItemPtr()										-- Pointer to each item at level 1
		local ptrFact = fhNewItemPtr()										-- Pointer to each fact returned to user
		ptrItem:MoveToFirstChildItem(ptrIndi)
		return function ()
			while ptrItem:IsNotNull() do
				ptrFact:MoveTo(ptrItem)
				ptrItem:MoveNext()
				if fhIsFact(ptrFact) then return ptrFact end
			end
		end
	end -- function Facts

	return fh

end -- local function iterate_v3

local iterate = iterate_v3()													-- To access FH iterate items module

--[[
@Module:			+fh+general_v3
@Author:			Mike Tate
@Version:			3.5
@LastUpdated:	12 Dec 2024
@Description:	A general functions module to supplement LUA functions, where filenames use UTF-8 but for a few exceptions.
@V3.5:				Further fix for Crossover/WINE file attributes;
@V3.4:				Further fix for Unix/WINE file attributes;
@V3.3:				Fix problem in fh.MakeFolder(...) when folder path is invalid; Remove Type, ShortName & ShortPath from attributes(...) as unsupported in WINE;
@V3.2:				Added function DetectOldModules(); Updated functions RenameFile(), RenameFolder() & GetFolderContents();
@V3.1:				Functions derived from FH V7 fhFileUtils library using File System Objects, plus additional features;
@V3.0:				Function Prototype Closure version; GetDayNumber() error message reasons;
@V1.5:				Revised SplitFilename(strFilename) for missing extension.
@V1.4:				Revised EstimatedBirthDates() & EstimatedDeathDates() to fix null Dates.
@V1.3:				Add GetDayNumber(), EstimatedBirthDates(), EstimatedDeathDates().
@V1.2:				SplitFilename() updated for directory only paths, and MakeFolder() added.
@V1.1:				pl.path experiment revoked. New DirTree with omit branch option. Avoid using stringx_v2.
@V1.0:				Initial version.
]]

local function general_v3()

	local fh = {}													-- Local environment table

	require("luacom")												-- To create File System Object
	fh.FSO = luacom.CreateObject("Scripting.FileSystemObject")

	-- Report error message --
	local function doError(strMessage,errFunction)
		-- strMessage		~ error message text
		-- errFunction		~ optional error reporting function
		if type(errFunction) == "function" then
			errFunction(strMessage)
		else
			error(strMessage)
		end
	end -- local function doError

	-- Convert filename to ANSI alternative and indicate success --
	function fh.FileNameToANSI(strFileName,strAnsiName)
		-- strFileName		~ full file path
		-- strAnsiFile		~ ANSI file name & type
		-- return values	~ ANSI file path, true if original path was ANSI compatible
		if stringx.encoding() == "ANSI" then return strFileName, true end
		local isFlag = fhIsConversionLossFlagSet()
		fhSetConversionLossFlag(false)
		local strAnsi = fhConvertUTF8toANSI(strFileName)
		local wasAnsi = true
		if fhIsConversionLossFlagSet() then
			strAnsiName = strAnsiName or "ANSI.ANSI"
			strAnsi = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"..strAnsiName
			wasAnsi = false
		end
		fhSetConversionLossFlag(isFlag)
		return strAnsi, wasAnsi
	end -- local function FileNameToANSI

	-- Get parent folder --
	function fh.GetParentFolder(strFileName)
		-- strFileName		~ full file path
		-- return value		~ parent folder path
		local strParent = fh.FSO:GetParentFolderName(strFileName)	--! Faulty in FH v6 with Unicode chars in path
		if fhGetAppVersion() == 6 then
			local _, wasAnsi = fh.FileNameToANSI(strFileName)
			if not wasAnsi then
				strParent = strFileName:match("^(.+)[\\/][^\\/]+[\\/]?$")
			end
		end
		return strParent
	end -- function GetParentFolder

	-- Check if file exists --
	function fh.FlgFileExists(strFileName)
		-- strFileName		~ full file path
		-- return value		~ true if it exists
		return fh.FSO:FileExists(strFileName)
	end -- function FlgFileExists

	-- Check if folder exists --
	function fh.FlgFolderExists(strFolderName)
		-- strFolderName	~ full file path
		-- return value		~ true if it exists
		return fh.FSO:FolderExists(strFolderName)
	end -- function FlgFolderExists

	-- Delete a file if it exists --
	function fh.DeleteFile(strFileName,errFunction)
		-- strFileName		~ full file path
		-- errFunction		~ optional error reporting function
		-- return value		~ true if file does not exist or is deleted else false
		if fh.FSO:FileExists(strFileName) then
			fh.FSO:DeleteFile(strFileName,true)
			if fh.FSO:FileExists(strFileName) then
				doError("File Not Deleted:\n"..strFileName.."\n",errFunction)
				return false
			end
		end
		return true
	end -- function DeleteFile

	-- Delete a folder if it exists including contents --
	function fh.DeleteFolder(strFolderName,errFunction)
		-- strFolderName	~ full folder path
		-- errFunction		~ optional error reporting function
		-- return value		~ true if folder does not exist or is deleted else false
		if fh.FSO:FolderExists(strFolderName) then
			fh.FSO:DeleteFolder(strFolderName,true)
			if fh.FSO:FolderExists(strFolderName) then
				doError("Folder Not Deleted:\n"..strFolderName.."\n",errFunction)
				return false
			end
		end
		return true
	end -- function DeleteFolder

	-- Rename a file if it exists --
	function fh.RenameFile(strFileName,strNewName)
		-- strFileName		~ full file path
		-- strNewName		~ new file name & type
		-- return value		~ true if file exists but new name does not and rename is OK else false
		local strNewFile = fh.GetParentFolder(strFileName).."\\"..strNewName
		if fh.FSO:FileExists(strFileName) and not fh.FSO:FileExists(strNewFile) then
			local fileObject = fh.FSO:GetFile(strFileName)
			fileObject.Name = strNewName
			if fh.FSO:FileExists(strNewFile) then
				return true
			end
		end
		return false
	end -- function RenameFile

	-- Rename a folder if it exists --
	function fh.RenameFolder(strFolderName,strNewName)
		-- strFolderName	~ full folder path
		-- strNewName		~ new folder name
		-- return value		~ true if folder exists but new name does not and rename is OK else false
		local strNewFolder = fh.GetParentFolder(strFolderName).."\\"..strNewName
		if fh.FSO:FolderExists(strFolderName) and not fh.FSO:FolderExists(strNewFolder) then
			local folderObject = fh.FSO:GetFolder(strFolderName)
			folderObject.Name = strNewName
			if fh.FSO:FolderExists(strNewFolder) then
				return true
			end
		end
		return false
	end -- function RenameFolder

	-- Copy a file if it exists and destination is not a folder --
	function fh.CopyFile(strFileName,strDestination)
		-- strFileName		~ full source file path
		-- strDestination	~ full target file path
		-- return value		~ true if file exists and is copied else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
			fh.FSO:CopyFile(strFileName,strDestination)
			if fh.FSO:FileExists(strDestination) then
				return true
			end
		end
		return false
	end -- function CopyFile

	-- Copy a folder if it exists and destination is not a file --
	function fh.CopyFolder(strFolderName,strDestination)
		-- strFolderName	~ full source folder path
		-- strDestination	~ full target folder path
		-- return value		~ true if folder exists and is copied else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
			fh.FSO:CopyFolder(strFolderName,strDestination)
			if fh.FSO:FolderExists(strDestination) then
				return true
			end
		end
		return false
	end -- function CopyFolder

	-- Move a file if it exists and destination is not a folder --
	function fh.MoveFile(strFileName,strDestination)
		-- strFileName		~ full source file path
		-- strDestination	~ full target file path
		-- return value		~ true if file exists and is moved else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
			if fh.DeleteFile(strDestination) then
				fh.FSO:MoveFile(strFileName,strDestination)
				if fh.FSO:FileExists(strDestination) then
					return true
				end
			end
		end
		return false
	end -- function MoveFile

	-- Move a folder if it exists and destination is not a file --
	function fh.MoveFolder(strFolderName,strDestination)
		-- strFolderName	~ full source folder path
		-- strDestination	~ full target folder path
		-- return value		~ true if folder exists and is moved else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
			if fh.DeleteFolder(strDestination) then
				fh.FSO:MoveFolder(strFolderName,strDestination)
				if fh.FSO:FolderExists(strDestination) then
					return true
				end
			end
		end
		return false
	end -- function MoveFolder

	local function CreateFolder(strFolderName)				-- V3.3
		fh.FSO:CreateFolder(strFolderName)
	end -- local function CreateFolder(strFolderName)

	-- Make subfolder recursively if does not exist --
	function fh.MakeFolder(strFolderName,errFunction)
		-- strFolderName	~ full source folder path
		-- errFunction		~ optional error reporting function
		-- return value		~ true if folder exists or created else false
		if not fh.FSO:FolderExists(strFolderName) then
			if #strFolderName > 4									-- V3.3
			and not fh.MakeFolder(fh.GetParentFolder(strFolderName),errFunction) then
				return false
			end
			if not pcall(CreateFolder,strFolderName)			-- V3.3
			or not fh.FSO:FolderExists(strFolderName) then
				doError("Cannot Make Folder Path:                 \n"..strFolderName.." \n",errFunction)
				return false
			end
		end
		return true
	end -- function MakeFolder

	-- Check if folder writable --
	function fh.FlgFolderWrite(strFolderName)
		-- strFolderName	~ full source folder path
		-- return value		~ true if folder writable else false
		if fh.FlgFolderExists(strFolderName) then
			if fh.MakeFolder(strFolderName.."\\vwxyz") then
				fh.FSO:DeleteFolder(strFolderName.."\\vwxyz",true)
				return true
			end
		end
		return false
	end -- function FlgFolderWrite

	-- Open File with ANSI path and return Handle --
	function fh.OpenFile(strFileName,strMode)
		-- strFileName		~ full file path
		-- strMode			~ "r", "w", "a" optionally suffixed with "+" &/or "b"
		-- return value		~ file handle
		local fileHandle, strError = io.open(strFileName,strMode)
		if fileHandle == nil then
			error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..strError.." \n")
		end
		return fileHandle
	end -- function OpenFile

	-- Save string to file --
	function fh.SaveStringToFile(strContents,strFileName,strFormat)
		-- strContents		~ text string
		-- strFileName		~ full file path
		-- strFormat			~ optional "UTF-8" or "UTF-16LE"
		-- return value		~ true if successful else false
		strFormat = strFormat or "UTF-8"
		if fhGetAppVersion() > 6 then
			return fhSaveTextFile(strFileName,strContents,strFormat)
		end
		local strAnsi, wasAnsi = fh.FileNameToANSI(strFileName)
		local fileHandle = fh.OpenFile(strAnsi,"w")
		fileHandle:write(strContents)
		assert(fileHandle:close())
		if not wasAnsi then
			fh.MoveFile(strAnsi,strFileName)
		end
		return true
	end -- function SaveStringToFile

	-- Load string from file --
	function fh.StrLoadFromFile(strFileName,strFormat)
		-- strFileName		~ full file path
		-- strFormat			~ optional "UTF-8" or "UTF-16LE"
		-- return value		~ file contents
		strFormat = strFormat or "UTF-8"
		if fhGetAppVersion() > 6 then
			return fhLoadTextFile(strFileName,strFormat)
		end
		local strAnsi, wasAnsi = fh.FileNameToANSI(strFileName)
		if not wasAnsi then
			fh.CopyFile(strFileName,strAnsi)
		end
		local fileHandle = fh.OpenFile(strAnsi,"r")
		local strContents = fileHandle:read("*all")
		assert(fileHandle:close())
		return strContents
	end -- function StrLoadFromFile

	-- Returns the Path, Filename, and Extension as 3 values --
	function fh.SplitFilename(strFileName)
		-- strFileName		~ full file path
		-- return values	~ path, name.type, type
		if fh.FSO:FolderExists(strFileName) then
			local strPath = strFileName:gsub("[\\/]$","")
			return strPath.."\\","",""
		end
		strFileName = strFileName.."."
		return strFileName:match("^(.-)([^\\/]-%.([^\\/%.]-))%.?$")
	end -- function SplitFilename

	-- Convert dd/mm/yyyy hh:mm:ss format to integer seconds -- (DateTime format is used in attributes returned by GetFolderContents and DirTree below)
	function fh.IntTime(strDateTime)
		-- strDateTime		~ date time string
		-- return value		~ integer seconds since 01/01/1970 00:00:00 
		local strDay,strMonth,strYear,strHour,strMin,strSec = strDateTime:match("^(%d%d)/(%d%d)/(%d+) (%d%d):(%d%d):(%d%d)")
		if tonumber(strYear) < 1970 then return 0 end
		local isDST = false
		if tonumber(strMonth) > 4 and tonumber(strMonth) < 11 then isDST = true end	-- Approximation is sometimes wrong
		local intTime = os.time( { year=strYear; month=strMonth; day=strDay; hour=strHour; min=strMin; sec=strSec; isdst=isDST; } )
		local tblDat = os.date("*t",intTime)
		if tblDat.isdst then
			intTime = intTime + 3600
			isDST = true
		end
		return intTime
	end -- function IntTime

	-- Return table of attributes for folder --
	local function attribFolder(tblAttr)
		-- tblAttr		~ folder attributes table
		-- return value	~ attributes table like LFS except datetimes
		-- WINE only supports the tblAttr.name & tblAttr.path
		return { mode="directory"; name=tblAttr.name; path=tblAttr.path; }
	end -- local function attribFolder

	-- Return table of attributes for file --
	local function attribFile(tblAttr)
		-- tblAttr		~ file attributes table
		-- return value	~ attributes table like LFS except datetimes
		-- WINE does not support the tblAttr.type, tblAttr.shortname, tblAttr.shortpath & sometimes tblAttr.datecreated
		return { mode="file"; name=tblAttr.name; path=tblAttr.path; size=tblAttr.size; modified=tblAttr.datelastmodified; attributes=tblAttr.attributes; }
	end -- local function attribFile

	-- Return attributes table of all files and folders in a specified folder --
	function fh.GetFolderContents(strFolder,doRecurse)
		-- strFolder		~ full folder path
		-- doRecurse		~ true for recursion
		-- return value	~ attributes table
		local arrList = {}
		if fh.FSO:FolderExists(strFolder) then

			local function getFileList(strFolder)
				local tblList = fh.FSO:GetFolder(strFolder)
				local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
				local tblAttr = tblEnum:Next()
				while tblAttr do
					table.insert(arrList,attribFolder(tblAttr))
					if doRecurse then getFileList(tblAttr.path) end
					tblAttr = tblEnum:Next()
				end
				local tblEnum = luacom.GetEnumerator(tblList.Files)
				local tblAttr = tblEnum:Next()
				while tblAttr do
					table.insert(arrList,attribFile(tblAttr))
					tblAttr = tblEnum:Next()
				end
			end

			getFileList(strFolder)
		end
		return arrList
	end -- function GetFolderContents

	-- Return a Directory Tree entry & attributes on each iteration --
	function fh.DirTree(strDir,...)
		-- strDir			~ full folder path
		-- ...				~ list of folders to omit
		-- return value	~ full path, attributes table
		local arg = {...}
		assert( fh.FSO:FolderExists(strDir), "directory parameter is missing or empty" )

		local function yieldtree(strDir)
			local tblList = fh.FSO:GetFolder(strDir)
			local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
			local tblAttr = tblEnum:Next()
			while tblAttr do	--	for _,tblAttr in luacom.pairs(tblList.SubFolders) do	-- pairs not working in FH v6 so use tblEnum code
				coroutine.yield(tblAttr.path,attributes(tblAttr,"directory"))
				local isOK = true
				for _,strOmit in ipairs (arg) do
					if tblAttr.path:match(strOmit) then 	-- Omit tree branch
						isOK = false
						break
					end
				end
				if isOK then yieldtree(tblAttr.path) end
				tblAttr = tblEnum:Next()
			end
			local tblEnum = luacom.GetEnumerator(tblList.Files)
			local tblAttr = tblEnum:Next()
			while tblAttr do	--	for _,tblAttr in luacom.pairs(tblList.Files) do		-- pairs not working in FH v6 so use tblEnum code
				coroutine.yield(tblAttr.path,attributes(tblAttr,"file"))
				tblAttr = tblEnum:Next()
			end
		end

		return coroutine.wrap(function() yieldtree(strDir) end)
	end -- function DirTree

	-- Detect FH V5/6 old library modules and advise removal --
	function fh.DetectOldModules()
		if fhGetAppVersion() > 6 then
			local strPath = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugins\\"
			local arrFile = { "compat53.lua"; "ltn12.lua"; "luasql\\sqlite3.dll"; "md5.lua"; "pl\\init.lua"; "socket.lua"; "utf8.lua"; "zip.dll"; }
			for _, strFile in ipairs (arrFile) do
				if fh.FSO:FileExists(strPath..strFile) then
					fhMessageBox("\n  Detected some old FH V6 library modules. \n\nPlease remove them by running the plugin: \n\n     'Delete old FH6 Plugin Module Files' \n","MB_OK","MB_ICONEXCLAMATION")
					break
				end
			end
		end
	end -- function DetectOldModules

	if fhGetAppVersion() > 6 then unpack = table.unpack end

	-- Invoke FH Shell Execute API --
	function fh.DoExecute(strExecutable,...)
		-- strExecutable	~ full path of executable
		-- ...					~ parameter list and optional error reporting function
		-- return value		~ true if successful else false
		local arg = {...}
		local errFunction = fhMessageBox
		if type(arg[#arg]) == 'function' then
			errFunction = arg[#arg]
			table.remove(arg)
		end
		local isOK, intErrorCode, strErrorText = fhShellExecute(strExecutable,unpack(arg))
		if not isOK then
			errFunction(tostring(strErrorText).." ("..tostring(intErrorCode)..")")
		end
		return isOK
	end -- function DoExecute

	-- Obtain the Day Number for any Date Point --									-- Fix problems with invalid dates in DayNumber function
	function fh.GetDayNumber(datDate)
		-- datDate		~ date point
		-- return value	~ day number
		if datDate:IsNull() then return 0 end
		local intDay = fhCallBuiltInFunction("DayNumber",datDate) 				-- Only works for Gregorian dates that were not skipped nor BC dates
		if not intDay then
			local strError = "because "													-- Error message reason	-- V3.0
			local calendar = datDate:GetCalendar()
			local oldMonth = datDate:GetMonth()
			local oldDayNo = datDate:GetDay()
			local intMonth = math.min( oldMonth, 12 ) 								-- Limit month to 12, and day to last of each month
			local intDayNo = math.min( oldDayNo, ({0;31;28;31;30;31;30;31;31;30;31;30;31;})[intMonth+1] )
			local intYear  = datDate:GetYear()
			if oldDayNo > intDayNo then strError = strError.."day "..oldDayNo.." too big "   end
			if oldMonth > intMonth then strError = strError.."month "..oldMonth.." too big " end
			if calendar == "Hebrew" and intYear > 3761 then
				intYear = intYear - 3761
				strError = strError.."Hebrew year > 3761 "
			elseif calendar ~= "Gregorian" then
				strError = strError..calendar.." disallowed "
			end
			if     intYear == 1752 and intMonth ==  9 and intDayNo <= 13 then	-- Use 2 Sep 1752 for 3 - 13 Sep 1752 dates skipped
				intDayNo = 2
				strError = strError.."3 - 13 Sep 1752 skipped "
			elseif intYear == 1582 and intMonth == 10 and intDayNo <= 14 then	-- Use 4 Oct 1582 for 5 - 14 Oct 1582 dates skipped
				intDayNo = 4
				strError = strError.."5 - 14 Oct 1582 skipped "
			end	
			local setDate = fhNewDatePt(intYear,intMonth,intDayNo,datDate:GetYearDD())
			intDay = fhCallBuiltInFunction("DayNumber",setDate) 					-- Remove BC and Julian, Hebrew, French calendars
			if not intDay then intDay = 0 end
			local oldDate = fhNewDate()		oldDate:SetSimpleDate(datDate)		-- Report problem to user
			local newDate = fhNewDate()		newDate:SetSimpleDate(setDate)
			local strIsBC = ""
			if datDate:GetBC() then
				strError = strError.." B.C. disallowed "
				intDay = -intDay
				strIsBC = "and Day Number negated"
			end
			fhMessageBox("\n Get Day Number issue for date \n "..oldDate:GetDisplayText().." \n "..strError.." \n So replaced it with date \n "..newDate:GetDisplayText().." \n "..strIsBC,"MB_OK","MB_ICONEXCLAMATION")
		end
		return intDay
	end -- function GetDayNumber

	local dtpYearMin = fhNewDatePt(1000)												-- Minimum year to use when earliest estimate is null
	local dtpYearMax = fhNewDatePt(2000)												-- Maximum year to use when latest estimate is null

	function fh.GetYearToday()																-- Get the Year for Today
		-- return value	~ integer year today
		local intYearToday = fhCallBuiltInFunction("Year",fhCallBuiltInFunction("Today"))
		dtpYearMax = fhNewDatePt(intYearToday)											-- Set maximum year date point
		return intYearToday
	end -- function GetYearToday()

	local function getDeathFacts(ptrIndi)												-- Iterate Death, Burial, Cremation facts
		-- ptrIndi		~ pointer to individual
		-- return value	~ pointer to fact
		local arrFact = { "~.DEAT"; "~.BURI"; "~.CREM"; }
		local intFact = 0
		local ptrFact = fhNewItemPtr()													-- Pointer to each fact returned to user
		return function ()
			while intFact < #arrFact do
				intFact = intFact + 1
				ptrFact = fhGetItemPtr(ptrIndi,arrFact[intFact])
				if ptrFact:IsNotNull() then return ptrFact end
			end
		end
	end -- local function getDeathFacts

	-- Ensure Estimated Date EARLIEST <= LATEST <= Fact Date --					-- Fix errors in EstimatedBirth/DeathDate function
	local function estimatedDates(strFunc,ptrIndi,intGens,getFact,intYrs)
		-- strFunc			~ "EstimatedBirthDate" or "EstimatedDeathDate"
		-- ptrIndi			~ Individual of interest
		-- intGens			~ Number of generations (may be nil)
		-- getFact			~ Iterator function for facts
		-- intYrs 			~ Years to add to After dates
		-- return values	~ EARLIEST, MID, LATEST dates
		intGens = intGens or 2
		local dtpMin = fhCallBuiltInFunction(strFunc,ptrIndi,"EARLIEST",intGens)
		local dtpMax = fhCallBuiltInFunction(strFunc,ptrIndi,"LATEST",intGens)
		local dtpMid = fhNewDatePt()
		if not ( dtpMin:IsNull() and dtpMax:IsNull() ) then 						-- Skip if both null	
			if dtpMax:IsNull() then dtpMax = dtpYearMax elseif dtpMin:IsNull() then dtpMin = dtpYearMin end
			for ptrFact in getFact(ptrIndi) do
				local datFact = fhGetValueAsDate(fhGetItemPtr(ptrFact,"~.DATE"))
				if not datFact:IsNull() then												-- Find 1st Fact Date
					local dtpLast = datFact:GetDatePt1()								-- Last date = DatePt1 for Simple, Range, and Before
					local strType = datFact:GetSubtype()								-- Between = DatePt2 and After = DatePt1 + intYrs
					if   strType == "Between" then dtpLast = datFact:GetDatePt2()
					elseif strType == "After" then dtpLast = fhNewDatePt(dtpLast:GetYear()+intYrs,dtpLast:GetMonth(),dtpLast:GetDay()) end		-- Compare only uses Year, Month, Day so omitted	,dtpLast:GetYearDD(),dtpLast:GetBC(),dtpLast:GetCalendar()
					if dtpMax:Compare(dtpLast) > 0 then dtpMax = dtpLast end
					if dtpMin:Compare(dtpMax)  > 0 then dtpMin = dtpMax  end
					if strType ~= "After" then break end								-- Now EARLIEST <= LATEST <= Last date
				end
			end	
			local intDays = ( fh.GetDayNumber(dtpMax) - fh.GetDayNumber(dtpMin) ) / 2
			local intYear,remYear = math.modf( intDays / 365.2422 )				-- Offset year @ 365.2422 days per year, and remainder fraction
			local intMnth = math.floor( ( remYear * 12 ) + 0.1 )					-- Offset month is remainder fraction of year * 12
			dtpMid = fhCallBuiltInFunction("CalcDate",dtpMin,intYear,intMnth)	-- Need approximate MID year & month
		end
		return { Min=dtpMin; Mid=dtpMid; Max=dtpMax; }								-- Return EARLIEST, MID, LATEST dates
	end -- local function estimatedDates

	-- Make EstimatedBirthDate EARLIEST <= LATEST <= 1st Fact Date --			-- Fix errors in EstimatedBirthDate function
	function fh.EstimatedBirthDates(ptrIndi,intGens)
		-- ptrInd				~ pointer to individual
		-- intGens			~ generations to include
		-- return values	~ EARLIEST, MID, LATEST dates
		return estimatedDates("EstimatedBirthDate",ptrIndi,intGens,iterate.Facts,10)
	end -- function EstimatedBirthDates

	-- Make EstimatedDeathDate EARLIEST <= LATEST <= DEAT/BURI/CREM Date --	-- Fix errors in EstimatedDeathDate function
	function fh.EstimatedDeathDates(ptrIndi,intGens)
		-- ptrInd				~ pointer to individual
		-- intGens			~ generations to include
		-- return values	~ EARLIEST, MID, LATEST dates
		return estimatedDates("EstimatedDeathDate",ptrIndi,intGens,getDeathFacts,100)
	end -- function EstimatedDeathDates

	--[[
	@function:		BuildDataRef
	@description:	Get Full Data Reference for Pointer
	@parameters:		Item Pointer
	@returns:			Data Reference String, Record Id Integer, Record Type Tag String
	@requires:		None
	]]
	function fh.BuildDataRef(ptrRef)

		local strDataRef = ""										-- Data Reference with instance indices e.g. INDI.RESI[2].ADDR
		local intRecId   = 0										-- Record Id for associated Record
		local strRecTag  = ""										-- Record Tag of associated Record type i.e. INDI, FAM, NOTE, SOUR, etc

		-- getDataRef() is called recursively per level of the Data Ref
		-- ptrRef points to the upper Data Ref levels yet to be analysed
		-- strRef compiles the lower Data Ref levels including instances

		local function getDataRef(ptrRef,strRef)
			local ptrTag = ptrRef:Clone()
			local strTag = fhGetTag(ptrTag)						-- Current level Tag
			ptrTag:MoveToParentItem(ptrTag)
			if ptrTag:IsNotNull() then							-- Parent level exists
				local intSib = 1
				local ptrSib = ptrRef:Clone()					-- Pointer to siblings with same Tag
				ptrSib:MovePrev("SAME_TAG")
				while ptrSib:IsNotNull() do						-- Count previous siblings with same Tag
					intSib = intSib + 1
					ptrSib:MovePrev("SAME_TAG")
				end
				if intSib > 1 then 	strTag = strTag.."["..intSib.."]" end
				getDataRef(ptrTag,"."..strTag..strRef)			-- Now analyse the parent level
			else
				strDataRef = strTag..strRef						-- Record level reached, so set return values
				intRecId   = fhGetRecordId(ptrRef)
				strRecTag  = strTag
				if not fhIsValidDataRef(strDataRef) then print("BuildDataRef: "..strDataRef.." is Invalid") end
			end
		end -- local function getDataRef

		if type(ptrRef) == "userdata" then getDataRef(ptrRef,"") end

		return strDataRef, intRecId, strRecTag

	end -- function BuildDataRef

	--[[
	@function:		GetDataRefPtr
	@description:	Get Pointer for Full Data Reference
	@parameters:		Data Reference String, Record Id Integer, Record Type Tag String (optional)
	@returns:			Item Pointer which IsNull() if any parameters are invalid
	@requires:		None
	]]
	function fh.GetDataRefPtr(strDataRef,intRecId,strRecTag)
		strDataRef = strDataRef or ""
		if not strRecTag then
			strRecTag = strDataRef:gsub("^(%u+).*$","%1")	-- Extract Record Tag from Data Ref
		end
		local ptrRef = fhNewItemPtr()
		ptrRef:MoveToRecordById(strRecTag,intRecId or 0)	-- Lookup the Record by Id
		ptrRef:MoveTo(ptrRef,strDataRef)						-- Move to the Data Ref
		return ptrRef
	end -- function GetDataRefPtr

	function fh.TblDataRef(ptrRef)
		local tblRef = {}
		tblRef.DataRef, tblRef.RecId, tblRef.RecTag = BuildDataRef(ptrRef)
		return tblRef
	end -- function TblDataRef

	function fh.PtrDataRef(tblRef)
		local tblRef = tblRef or {}								-- Ensure table and its fields exist
		return GetDataRefPtr(tblRef.DataRef or "",tblRef.RecId or 0,tblRef.RecTag or "")
	end -- function PtrDataRef

	return fh

end -- local function general_v3

local general = general_v3()										-- To access FH general tools module

--[[
@Module:			+fh+tablex_v3
@Author:			Mike Tate
@Version:			3.1
@LastUpdated:	08 Jan 2022
@Description:	A Table Load Save Module.
@V3.1:				Cater for full UTF-8 filenames.
@V3.0:				Function Prototype Closure version.
@V1.2:				Added local definitions of _ to ensure nil gets returned on error.
@V1.1:				?
@V1.0:				Initial version 0.94 is Lua 5.1 compatible.
]]

local function tablex_v3()

	local fh = {}									-- Local environment table

	------------------------------------------------------ Start Table Load Save
	-- require "_tableloadsave"
	--[[
	Save Table to File/Stringtable
	Load Table from File/Stringtable
	v 0.94

	Lua 5.1 compatible

	Userdata and indices of these are not saved
	Functions are saved via string.dump, so make sure it has no upvalues
	References are saved
	----------------------------------------------------
	table.save( table [, filename] )

	Saves a table so it can be called via the table.load function again
	table must a object of type 'table'
	filename is optional, and may be a string representing a filename or true/1

	table.save( table )
		on success: returns a string representing the table (stringtable)
		(uses a string as buffer, ideal for smaller tables)
	table.save( table, true or 1 )
		on success: returns a string representing the table (stringtable)
		(uses io.tmpfile() as buffer, ideal for bigger tables)
	table.save( table, "filename" )
		on success: returns 1
		(saves the table to file "filename")
	on failure: returns as second argument an error msg
	----------------------------------------------------
	table.load( filename or stringtable )

	Loads a table that has been saved via the table.save function

	on success: returns a previously saved table
	on failure: returns as second argument an error msg
	----------------------------------------------------

	chillcode, http://lua-users.org/wiki/SaveTableToFile
	Licensed under the same terms as Lua itself.
	]]--

	-- declare local variables
	--// exportstring( string )
	--// returns a "Lua" portable version of the string
	local function exportstring( s )
		s = string.format( "%q",s )
		-- to replace
		s = string.gsub( s,"\\\n","\\n" )
		s = string.gsub( s,"\r","\\r" )
		s = string.gsub( s,string.char(26),"\"..string.char(26)..\"" )
		return s
	end
--// The Save Function
function fh.save( tbl,filename )
	local charS,charE = "   ","\n"
	local file,err,_,stransi,wasansi 					-- V1.2 -- V3.1 -- Added _,stransi,wasansi	--!
	-- create a pseudo file that writes to a string and return the string
	if not filename then
		file =  { write = function( self,newstr ) self.str = self.str..newstr end, str = "" }
		charS,charE = "",""
	-- write table to tmpfile
	elseif filename == true or filename == 1 then
		charS,charE,file = "","",io.tmpfile()
	-- write table to file
	-- use io.open here rather than io.output, since in windows when clicking on a file opened with io.output will create an error
	else
		stransi,wasansi = general.FileNameToANSI(filename)	-- V3.1 -- Cater for non-ANSI filename	--!
		file,err = io.open( stransi, "w" )
		if err then return _,err end
	end
	-- initiate variables for save procedure
	local tables,lookup = { tbl },{ [tbl] = 1 }
	file:write( "return {"..charE )
	for idx,t in ipairs( tables ) do
		if filename and filename ~= true and filename ~= 1 then
			file:write( "-- Table: {"..idx.."}"..charE )
		end
		file:write( "{"..charE )
		local thandled = {}
		for i,v in ipairs( t ) do
			thandled[i] = true
			-- escape functions and userdata
			if type( v ) ~= "userdata" then
				-- only handle value
				if type( v ) == "table" then
					if not lookup[v] then
						table.insert( tables, v )
						lookup[v] = #tables
					end
					file:write( charS.."{"..lookup[v].."},"..charE )
				elseif type( v ) == "function" then
					file:write( charS.."loadstring("..exportstring(string.dump( v )).."),"..charE )
				else
					local value =  ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
					file:write(  charS..value..","..charE )
				end
			end
		end
		for i,v in pairs( t ) do
			-- escape functions and userdata
			if (not thandled[i]) and type( v ) ~= "userdata" then
				-- handle index
				if type( i ) == "table" then
					if not lookup[i] then
						table.insert( tables,i )
						lookup[i] = #tables
					end
					file:write( charS.."[{"..lookup[i].."}]=" )
				else
					local index = ( type( i ) == "string" and "["..exportstring( i ).."]" ) or string.format( "[%d]",i )
					file:write( charS..index.."=" )
				end
				-- handle value
				if type( v ) == "table" then
					if not lookup[v] then
						table.insert( tables,v )
						lookup[v] = #tables
					end
					file:write( "{"..lookup[v].."},"..charE )
				elseif type( v ) == "function" then
					file:write( "loadstring("..exportstring(string.dump( v )).."),"..charE )
				else
					local value =  ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
					file:write( value..","..charE )
				end
			end
		end
		file:write( "},"..charE )
	end
	file:write( "}" )
	-- Return Values
	-- return stringtable from string
	if not filename then
		-- set marker for stringtable
		return file.str.."--|"
	-- return stringttable from file
	elseif filename == true or filename == 1 then
		file:seek ( "set" )
		-- no need to close file, it gets closed and removed automatically
		-- set marker for stringtable
		return file:read( "*a" ).."--|"
	-- close file and return 1
	else
		file:close()
		if not ( wasansi ) then		-- V3.1 -- Cater for non-ANSI filename	--!
			general.MoveFile(stransi,filename)
		end
		return 1
	end
end

--// The Load Function
function fh.load( sfile )
	local tables,err,_ 					-- V1.2 -- Added _
	-- catch marker for stringtable
	if string.sub( sfile,-3,-1 ) == "--|" then
		tables,err = loadstring( sfile )
	else
		local stransi,wasansi = general.FileNameToANSI(sfile)	-- V3.1 -- Cater for non-ANSI filename	--!
		if not ( wasansi ) then
			general.CopyFile(sfile,stransi)
		end
		tables,err = loadfile( stransi )
		if not ( wasansi ) then
			general.DeleteFile(stransi)								-- V3.1 -- Cater for non-ANSI filename	--!
		end
	end
	if err then return _,err
	end
	tables = tables()
	for idx = 1,#tables do
		local tolinkv,tolinki = {},{}
		for i,v in pairs( tables[idx] ) do
			if type( v ) == "table" and tables[v[1]] then
				table.insert( tolinkv,{ i,tables[v[1]] } )
			end
			if type( i ) == "table" and tables[i[1]] then
				table.insert( tolinki,{ i,tables[i[1]] } )
			end
		end
		-- link values, first due to possible changes of indices
		for _,v in ipairs( tolinkv ) do
			tables[idx][v[1]] = v[2]
		end
		-- link indices
		for _,v in ipairs( tolinki ) do
			tables[idx][v[2]],tables[idx][v[1]] =  tables[idx][v[1]],nil
		end
	end
	return tables[1]
end

------------------------------------------------------ End Table Load Save

	-- overload fh functions into table
	for strIndex, anyValue in pairs(fh) do
		if type(anyValue) == "function" then
			table[strIndex] = anyValue
		end
	end

	return fh

end -- local function tablex_v3

local tablex  = tablex_v3 ()													-- To access FH table extension module

--[[
@Module:			+fh+encoder_v3
@Author:			Mike Tate
@Version:			3.6
@LastUpdated:	27 Aug 2024
@Description:	Text encoder module for HTML XHTML XML URI UTF8 UTF16 ISO CP1252/ANSI character codings.
@V3.6:				In fh.FileLines(...) cater for empty file;
@V3.5:				Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility.
@V3.4:				Ensure expressions involving gsub return just text parameter.
@V3.3:				Adds UNICODE U+10000 to U+10FFFF UTF-16 Supplementary Planes.
@V3.2:				Update for ANSI & Unicode to ASCII for sorting, Soundex, etc.
@V3.1:				Update for Unicode UTF-16 & UTF-8 and fhConvertANSItoUTF8 & fhConvertUTF8toANSI, name change UTF to UTF8 & CP to ANSI.
@V2.0:				StrUTF8_Encode() replaced by StrUTF_CP1252() for entire UTF-8 range, plus new StrCP1252_ISO().
@V1.0:				Initial version.
]]

local function encoder_v3()

	local fh = {}													-- Local environment table

	local fhVersion = fhGetAppVersion()

	local br_Tag = "<br />"										-- Markup language break tag default
	local br_Lua = "<br ?/?>"										-- Lua pattern for break tag recognition

	local tblCodePage = {}											-- Code Page to XML/XHTML/HTML/URI/UTF8 encodings: http://en.wikipedia.org/wiki/Windows-1252 & 1250 & etc

	-- Control characters "\000" to "\031" for URI & Markup "[%c]" encodings are disallowed except for "\t" to "\r"

	tblCodePage["\000"] = ""	-- NUL
	tblCodePage["\001"] = ""	-- SOH
	tblCodePage["\002"] = ""	-- STX
	tblCodePage["\003"] = ""	-- ETX
	tblCodePage["\004"] = ""	-- EOT
	tblCodePage["\005"] = ""	-- ENQ
	tblCodePage["\006"] = ""	-- ACK
	tblCodePage["\a"] = ""		-- BEL
	tblCodePage["\b"] = ""		-- BS
	tblCodePage["\t"] = "+"	-- HT	space  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["\n"] = "%0A"	-- LF	br_Tag in Markup
	tblCodePage["\v"] = "%0A"	-- VT	br_Tag in Markup
	tblCodePage["\f"] = "%0A"	-- FF	br_Tag in Markup
	tblCodePage["\r"] = "%0D"	-- CR	br_Tag in Markup
	tblCodePage["\014"] = ""	-- SO
	tblCodePage["\015"] = ""	-- SI
	tblCodePage["\016"] = ""	-- DLE
	tblCodePage["\017"] = ""	-- DC1
	tblCodePage["\018"] = ""	-- DC2
	tblCodePage["\019"] = ""	-- DC3
	tblCodePage["\020"] = ""	-- DC4
	tblCodePage["\021"] = ""	-- NAK
	tblCodePage["\022"] = ""	-- SYN
	tblCodePage["\023"] = ""	-- ETB
	tblCodePage["\024"] = ""	-- CAN
	tblCodePage["\025"] = ""	-- EM
	tblCodePage["\026"] = ""	-- SUB
	tblCodePage["\027"] = ""	-- ESC
	tblCodePage["\028"] = ""	-- FS
	tblCodePage["\029"] = ""	-- GS
	tblCodePage["\030"] = ""	-- RS
	tblCodePage["\031"] = ""	-- US

	-- ASCII characters "\032" to "\127" for URI "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding

	tblCodePage[" "] = "+"		-- or "%20"	Space
	tblCodePage["!"] = "%21"	-- Reserved character
	tblCodePage['"'] = "%22"								--	"&quot;" in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["#"] = "%23"	-- Reserved character
	tblCodePage["$"] = "%24"	-- Reserved character
	tblCodePage["%"] = "%25"	-- Must be encoded
	tblCodePage["&"] = "%26"	-- Reserved character	-- "&amp;"  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["'"] = "%27"	-- Reserved character	-- "&apos;" in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["("] = "%28"	-- Reserved character
	tblCodePage[")"] = "%29"	-- Reserved character
	tblCodePage["*"] = "%2A"	-- Reserved character
	tblCodePage["+"] = "%2B"	-- Reserved character
	tblCodePage[","] = "%2C"	-- Reserved character
--	tblCodePage["-"] = "%2D"	-- Unreserved character not encoded
--	tblCodePage["."] = "%2E"	-- Unreserved character not encoded
	tblCodePage["/"] = "%2F"	-- Reserved character
--	Digits 0 to 9				-- Unreserved characters not encoded
	tblCodePage[":"] = "%3A"	-- Reserved character
	tblCodePage[";"] = "%3B"	-- Reserved character
	tblCodePage["<"] = "%3C"								-- "&lt;"  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["="] = "%3D"	-- Reserved character
	tblCodePage[">"] = "%3E"								-- "&gt;"  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["?"] = "%3F"	-- Reserved character
	tblCodePage["@"] = "%40"	-- Reserved character
--	Letters A to Z				-- Unreserved characters not encoded
	tblCodePage["["] = "%5B"	-- Reserved character
	tblCodePage["\\"]= "%5C"
	tblCodePage["]"] = "%5D"	-- Reserved character
	tblCodePage["^"] = "%5E"
--	tblCodePage["_"] = "%5F"	-- Unreserved character not encoded
	tblCodePage["`"] = "%60"
--	Letters a to z				-- Unreserved characters not encoded
	tblCodePage["{"] = "%7B"
	tblCodePage["|"] = "%7C"
	tblCodePage["}"] = "%7D"
--	tblCodePage["~"] = "%7E"	-- Unreserved character not encoded
	tblCodePage["\127"] = ""	-- DEL

	-- Code Page 1252 Unicode characters "\128" to "\255" for UTF-8 scheme "[€-ÿ]" encodings: http://en.wikipedia.org/wiki/UTF-8

	tblCodePage["€"] = string.char(0xE2,0x82,0xAC)	-- "&euro;"
	tblCodePage["\129"] = ""  -- Undefined
	tblCodePage["‚"] = string.char(0xE2,0x80,0x9A)
	tblCodePage["ƒ"] = string.char(0xC6,0x92)
	tblCodePage["„"] = string.char(0xE2,0x80,0x9E)
	tblCodePage["…"] = string.char(0xE2,0x80,0xA6)
	tblCodePage["†"] = string.char(0xE2,0x80,0xA0)
	tblCodePage["‡"] = string.char(0xE2,0x80,0xA1)
	tblCodePage["ˆ"] = string.char(0xCB,0x86)
	tblCodePage["‰"] = string.char(0xE2,0x80,0xB0)
	tblCodePage["Š"] = string.char(0xC5,0xA0)
	tblCodePage["‹"] = string.char(0xE2,0x80,0xB9)
	tblCodePage["Œ"] = string.char(0xC5,0x92)
	tblCodePage["\141"] = ""  -- Undefined
	tblCodePage["Ž"] = string.char(0xC5,0xBD)
	tblCodePage["\143"] = ""  -- Undefined
	tblCodePage["\144"] = ""  -- Undefined
	tblCodePage["‘"] = string.char(0xE2,0x80,0x98)
	tblCodePage["’"] = string.char(0xE2,0x80,0x99)
	tblCodePage["“"] = string.char(0xE2,0x80,0x9C)
	tblCodePage["”"] = string.char(0xE2,0x80,0x9D)
	tblCodePage["•"] = string.char(0xE2,0x80,0xA2)
	tblCodePage["–"] = string.char(0xE2,0x80,0x93)
	tblCodePage["—"] = string.char(0xE2,0x80,0x94)
	tblCodePage["\152"] = string.char(0xCB,0x9C)	-- Small Tilde
	tblCodePage["™"] = string.char(0xE2,0x84,0xA2)
	tblCodePage["š"] = string.char(0xC5,0xA1)
	tblCodePage["›"] = string.char(0xE2,0x80,0xBA)
	tblCodePage["œ"] = string.char(0xC5,0x93)
	tblCodePage["\157"] = ""  -- Undefined
	tblCodePage["ž"] = string.char(0xC5,0xBE)
	tblCodePage["Ÿ"] = string.char(0xC5,0xB8)
	tblCodePage["\160"] = string.char(0xC2,0xA0)	-- "&nbsp;"	No Break Space
	tblCodePage["¡"] = string.char(0xC2,0xA1)		-- "&iexcl;"
	tblCodePage["¢"] = string.char(0xC2,0xA2)		-- "&cent;"
	tblCodePage["£"] = string.char(0xC2,0xA3)		-- "&pound;"
	tblCodePage["¤"] = string.char(0xC2,0xA4)		-- "&curren;"
	tblCodePage["¥"] = string.char(0xC2,0xA5)		-- "&yen;"
	tblCodePage["¦"] = string.char(0xC2,0xA6)
	tblCodePage["§"] = string.char(0xC2,0xA7)
	tblCodePage["¨"] = string.char(0xC2,0xA8)
	tblCodePage["©"] = string.char(0xC2,0xA9)
	tblCodePage["ª"] = string.char(0xC2,0xAA)
	tblCodePage["«"] = string.char(0xC2,0xAB)
	tblCodePage["¬"] = string.char(0xC2,0xAC)
	tblCodePage["­"] = string.char(0xC2,0xAD)		-- "&shy;"	Soft Hyphen
	tblCodePage["®"] = string.char(0xC2,0xAE)
	tblCodePage["¯"] = string.char(0xC2,0xAF)
	tblCodePage["°"] = string.char(0xC2,0xB0)
	tblCodePage["±"] = string.char(0xC2,0xB1)
	tblCodePage["²"] = string.char(0xC2,0xB2)
	tblCodePage["³"] = string.char(0xC2,0xB3)
	tblCodePage["´"] = string.char(0xC2,0xB4)
	tblCodePage["µ"] = string.char(0xC2,0xB5)
	tblCodePage["¶"] = string.char(0xC2,0xB6)
	tblCodePage["·"] = string.char(0xC2,0xB7)
	tblCodePage["¸"] = string.char(0xC2,0xB8)
	tblCodePage["¹"] = string.char(0xC2,0xB9)
	tblCodePage["º"] = string.char(0xC2,0xBA)
	tblCodePage["»"] = string.char(0xC2,0xBB)
	tblCodePage["¼"] = string.char(0xC2,0xBC)
	tblCodePage["½"] = string.char(0xC2,0xBD)
	tblCodePage["¾"] = string.char(0xC2,0xBE)
	tblCodePage["¿"] = string.char(0xC2,0xBF)
	tblCodePage["À"] = string.char(0xC3,0x80)
	tblCodePage["Á"] = string.char(0xC3,0x81)
	tblCodePage["Â"] = string.char(0xC3,0x82)
	tblCodePage["Ã"] = string.char(0xC3,0x83)
	tblCodePage["Ä"] = string.char(0xC3,0x84)
	tblCodePage["Å"] = string.char(0xC3,0x85)
	tblCodePage["Æ"] = string.char(0xC3,0x86)
	tblCodePage["Ç"] = string.char(0xC3,0x87)
	tblCodePage["È"] = string.char(0xC3,0x88)
	tblCodePage["É"] = string.char(0xC3,0x89)
	tblCodePage["Ê"] = string.char(0xC3,0x8A)
	tblCodePage["Ë"] = string.char(0xC3,0x8B)
	tblCodePage["Ì"] = string.char(0xC3,0x8C)
	tblCodePage["Í"] = string.char(0xC3,0x8D)
	tblCodePage["Î"] = string.char(0xC3,0x8E)
	tblCodePage["Ï"] = string.char(0xC3,0x8F)
	tblCodePage["Ð"] = string.char(0xC3,0x90)
	tblCodePage["Ñ"] = string.char(0xC3,0x91)
	tblCodePage["Ò"] = string.char(0xC3,0x92)
	tblCodePage["Ó"] = string.char(0xC3,0x93)
	tblCodePage["Ô"] = string.char(0xC3,0x94)
	tblCodePage["Õ"] = string.char(0xC3,0x95)
	tblCodePage["Ö"] = string.char(0xC3,0x96)
	tblCodePage["×"] = string.char(0xC3,0x97)
	tblCodePage["Ø"] = string.char(0xC3,0x98)
	tblCodePage["Ù"] = string.char(0xC3,0x99)
	tblCodePage["Ú"] = string.char(0xC3,0x9A)
	tblCodePage["Û"] = string.char(0xC3,0x9B)
	tblCodePage["Ü"] = string.char(0xC3,0x9C)
	tblCodePage["Ý"] = string.char(0xC3,0x9D)
	tblCodePage["Þ"] = string.char(0xC3,0x9E)
	tblCodePage["ß"] = string.char(0xC3,0x9F)
	tblCodePage["à"] = string.char(0xC3,0xA0)
	tblCodePage["á"] = string.char(0xC3,0xA1)
	tblCodePage["â"] = string.char(0xC3,0xA2)
	tblCodePage["ã"] = string.char(0xC3,0xA3)
	tblCodePage["ä"] = string.char(0xC3,0xA4)
	tblCodePage["å"] = string.char(0xC3,0xA5)
	tblCodePage["æ"] = string.char(0xC3,0xA6)
	tblCodePage["ç"] = string.char(0xC3,0xA7)
	tblCodePage["è"] = string.char(0xC3,0xA8)
	tblCodePage["é"] = string.char(0xC3,0xA9)
	tblCodePage["ê"] = string.char(0xC3,0xAA)
	tblCodePage["ë"] = string.char(0xC3,0xAB)
	tblCodePage["ì"] = string.char(0xC3,0xAC)
	tblCodePage["í"] = string.char(0xC3,0xAD)
	tblCodePage["î"] = string.char(0xC3,0xAE)
	tblCodePage["ï"] = string.char(0xC3,0xAF)
	tblCodePage["ð"] = string.char(0xC3,0xB0)
	tblCodePage["ñ"] = string.char(0xC3,0xB1)
	tblCodePage["ò"] = string.char(0xC3,0xB2)
	tblCodePage["ó"] = string.char(0xC3,0xB3)
	tblCodePage["ô"] = string.char(0xC3,0xB4)
	tblCodePage["õ"] = string.char(0xC3,0xB5)
	tblCodePage["ö"] = string.char(0xC3,0xB6)
	tblCodePage["÷"] = string.char(0xC3,0xB7)
	tblCodePage["ø"] = string.char(0xC3,0xB8)
	tblCodePage["ù"] = string.char(0xC3,0xB9)
	tblCodePage["ú"] = string.char(0xC3,0xBA)
	tblCodePage["û"] = string.char(0xC3,0xBB)
	tblCodePage["ü"] = string.char(0xC3,0xBC)
	tblCodePage["ý"] = string.char(0xC3,0xBD)
	tblCodePage["þ"] = string.char(0xC3,0xBE)
	tblCodePage["ÿ"] = string.char(0xC3,0xBF)

	-- Set XML/XHTML/HTML "[%c\"&'<>]" Markup encodings: http://en.wikipedia.org/wiki/XML and http://en.wikipedia.org/wiki/HTML
	local function setMarkupEncodings()
		tblCodePage["\t"] = " "		-- HT	"\t" to "\r" are treated as white space in Markup Languages by default
		tblCodePage["\n"] = br_Tag	-- LF	
		tblCodePage["\v"] = br_Tag	-- VT	line break tag	"<br>" or "<br >" or "<br/>" or "<br />" is better
		tblCodePage["\f"] = br_Tag	-- FF
		tblCodePage["\r"] = br_Tag	-- CR
		tblCodePage['"']  = "&quot;"
		tblCodePage["&"]  = "&amp;"
		tblCodePage["'"]  = "&apos;"
		tblCodePage["<"]  = "&lt;"
		tblCodePage[">"]  = "&gt;"
	end -- local function setMarkupEncodings

	-- Set URI/URL/URN "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding
	local function setURIEncodings()
		tblCodePage["\t"] = "+"		-- HT	space
		tblCodePage["\n"] = "%0A" 	-- LF	newline
		tblCodePage["\v"] = "%0A" 	-- VT	newline
		tblCodePage["\f"] = "%0A" 	-- FF	newline
		tblCodePage["\r"] = "%0D" 	-- CR	return
		tblCodePage['"']  = "%22"
		tblCodePage["&"]  = "%26"
		tblCodePage["'"]  = "%27"
		tblCodePage["<"]  = "%3C"
		tblCodePage[">"]  = "%3E"
	end -- local function setURIEncodings

	-- Encode characters according to gsub pattern & lookup table --
	local function strEncode(strText,strPattern,tblPattern)
		return ( (strText or ""):gsub(strPattern,tblPattern) )		-- V3.4
	end -- local function strEncode

	-- Encode CP1252/ANSI characters into UTF-8 codes --
	function fh.StrANSI_UTF8(strText)
		if fhVersion > 5 then
			strText = fhConvertANSItoUTF8(strText)
		else
			strText = strEncode(strText,"[\127-ÿ]",tblCodePage)
		end
		return strText
	end -- function StrANSI_UTF8

	function fh.StrCP_UTF(strText)			-- Legacy
		return fh.StrANSI_UTF8(strText)
	end -- function StrCP1252_UTF8

	function fh.StrCP1252_UTF(strText)		-- Legacy
		return fh.StrANSI_UTF8(strText)
	end -- function StrCP1252_UTF

	-- Encode CP1252/ANSI or UTF-8 characters into UTF-8 --
	function fh.StrEncode_UTF8(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_UTF8(strText)
		else
			return strText
		end
	end -- function StrEncode_UTF8

	-- Encode CP1252/ANSI characters into XML/XHTML/HTML/UTF8 codes --
	local strANSI_XML = "[%z\001-\031\"&'<>\127-ÿ]"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strANSI_XML = "[\000-\031\"&'<>\127-ÿ]"
	end

	function fh.StrANSI_XML(strText)
		setMarkupEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes br_Tag
		strText = strEncode(strText,strANSI_XML,tblCodePage)
		return strText
	end -- function StrANSI_XML

	function StrCP_XML(strText)			-- Legacy
		return fh.StrANSI_XML(strText)
	end -- function StrCP_XML

	function StrCP1252_XML(strText)		-- Legacy
		return fh.StrANSI_XML(strText)
	end -- function StrCP1252_XML

	-- Encode UTF-8 ASCII characters into XML/XHTML/HTML codes --
	local strUTF8_XML = "[%z\001-\031\"&'<>\127]"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strUTF8_XML = "[\000-\031\"&'<>\127]"
	end

	function fh.StrUTF8_XML(strText)
		setMarkupEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes br_Tag
		strText = strEncode(strText,strUTF8_XML,tblCodePage)
		return strText
	end -- function StrUTF8_XML

	-- Encode CP1252/ANSI or UTF-8 ASCII characters into XML/XHTML/HTML codes --
	function fh.StrEncode_XML(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_XML(strText)
		else
			return fh.StrUTF8_XML(strText)
		end
	end -- function StrEncode_XML

	-- Encode Item Text characters into XML/HTML/UTF-8 codes --
	function fh.StrGetItem_XML(ptrItem,strTags)
		return fh.StrEncode_XML(fhGetItemText(ptrItem,strTags))
	end -- function StrGetItem_XML

	-- Encode CP1252/ANSI characters into URI codes --
	function fh.StrANSI_URI(strText)
		setURIEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes %0A
		strText = strEncode(strText,"[^0-9A-Za-z]",tblCodePage)
		return strText
	end -- function StrANSI_URI

	function fh.StrCP_URI(strText)
		return fh.StrANSI_URI(strText)
	end -- function StrCP_URI

	function fh.StrCP1252_URI(strText)
		return fh.StrANSI_URI(strText)
	end -- function StrCP1252_URI

	-- Encode UTF-8 ASCII characters into URI codes --
	local strUTF8_URI = "[%z\001-\127]"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strUTF8_URI = "[\000-\127]"
	end

	function fh.StrUTF8_URI(strText)
		setURIEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes br_Tag
		strText = strEncode(strText,strUTF8_URI,tblCodePage)
		return strText
	end -- function StrUTF8_URI

	-- Encode CP1252/ANSI or UTF-8 ASCII characters into URI codes --
	function fh.StrEncode_URI(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_URI(strText)
		else
			return fh.StrUTF8_URI(strText)
		end
	end -- function StrEncode_URI

	function fh.StrUTF8_Encode(strText) -- Legacy from V1.0
		return fh.StrUTF8_ANSI(strText)
	end -- function StrUTF8_Encode

	-- Encode UTF-8 bytes into single CP1252/ANSI character V2.0 upvalues --
	local strByteRange = "["..string.char(0xC0).."-"..string.char(0xFF).."]"
	local tblBytePoint = {0xC0;0xE0;0xF0;0xF8;0xFC;}		-- Byte codes for 2-byte, 3-byte, 4-byte, 5-byte, 6-byte UTF-8
	local tblUTF8 = {}
	for strByte = string.byte("€"), string.byte("ÿ") do
		local strChar = string.char(strByte)					-- Use CodePage to UTF-8 table to populate UTF-8 to CodePage table
		local strCode = tblCodePage[strChar]
		tblUTF8[strCode] = strChar
	end

	-- Encode UTF-8 bytes into single CP1252/ANSI character --
	function fh.StrUTF8_ANSI(strText)
		strText = strText or ""
		if fhVersion > 5 then return fhConvertUTF8toANSI(strText) end
		if strText:match(strByteRange) then						-- If text contains characters that need translating then
			local intChar = 0										-- Input character index
			local strChar = ""										-- Current character
			local strCode = ""										-- UTF-8 multi-byte code
			local tblLine = {}										-- Translated output line
			repeat
				intChar = intChar + 1								-- Step through each character in text
				strChar = strText:sub(intChar,intChar)
				if strChar:match(strByteRange) then				-- Convert UTF-8 bytes into CP character
					strCode = strChar								-- First UTF-8 byte code, whose top bits say how many bytes to append
					for intByte, strByte in ipairs(tblBytePoint) do
						if string.byte(strChar) >= strByte then
							intChar = intChar + 1					-- Append next UTF-8 byte code character
							strCode = strCode..strText:sub(intChar,intChar)
						else
							break
						end 
					end
					strChar = tblUTF8[strCode] or "¿"			-- Translate UTF-8 code into CP character
				end
				table.insert(tblLine,strChar)					-- Accumulate output char by char
			until intChar >= #strText
			strText = table.concat(tblLine)
		end
		return strText
	end -- function StrUTF8_ANSI

	function fh.StrUTF_CP(strText)			-- Legacy
		return fh.StrUTF8_ANSI(strText)
	end -- function StrUTF_CP

	function fh.StrUTF_CP1252(strText)		-- Legacy
		return fh.StrUTF8_ANSI(strText)
	end -- function StrUTF_CP1252

	-- Encode CP1252/ANSI or UTF-8 characters into ANSI --
	function fh.StrEncode_ANSI(strText)
		if stringx.encoding() == "ANSI" then
			return strText or ""
		else
			return fh.StrUTF8_ANSI(strText)
		end
	end -- function StrEncode_ANSI

	-- Set ISO-8859-1 "[\127-Ÿ]" encodings: http://en.wikipedia.org/wiki/ISO/IEC_8859-1
	local tblISO8859 = { }
	tblISO8859["\127"]=""  -- DEL
	tblISO8859["€"] = "EUR"	
	tblISO8859["\129"]=""  -- Undefined
	tblISO8859["‚"] = "¸"
	tblISO8859["ƒ"] = "f"
	tblISO8859["„"] = "¸¸"
	tblISO8859["…"] = "..."
	tblISO8859["†"] = "+"
	tblISO8859["‡"] = "±"
	tblISO8859["ˆ"] = "^"
	tblISO8859["‰"] = "%"
	tblISO8859["Š"] = "S"
	tblISO8859["‹"] = "<"
	tblISO8859["Œ"] = "OE"
	tblISO8859["\141"]=""  -- Undefined
	tblISO8859["Ž"] = "Z"
	tblISO8859["\143"]=""  -- Undefined
	tblISO8859["\144"]=""  -- Undefined
	tblISO8859["‘"] = "'"
	tblISO8859["’"] = "'"
	tblISO8859["“"] = '"'
	tblISO8859["”"] = '"'
	tblISO8859["•"] = "º"
	tblISO8859["–"] = "-"
	tblISO8859["—"] = "-"
	tblISO8859["\152"]="~" -- Small Tilde
	tblISO8859["™"] = "TM"
	tblISO8859["š"] = "s"
	tblISO8859["›"] = ">"
	tblISO8859["œ"] = "oe"
	tblISO8859["\157"]=""  -- Undefined
	tblISO8859["ž"] = "z"
	tblISO8859["Ÿ"] = "Y"

	-- Encode CP1252/ANSI characters into ISO-8859-1 codes --
	function fh.StrANSI_ISO(strText)
		return strEncode(strText,"[\127-Ÿ]",tblISO8859)
	end -- function StrANSI_ISO

	function fh.StrCP_ISO(strText)			-- Legacy
		return fh.StrANSI_ISO(strText)
	end -- function StrCP_ISO

	function fh.StrCP1252_ISO(strText)		-- Legacy
		return fh.StrANSI_ISO(strText)
	end -- function StrCP1252_ISO

	function fh.StrUTF8_ISO(strText)
		return fh.StrANSI_ISO(fh.StrUTF8_ANSI(strText))
	end -- function StrUTF8_ISO

	-- Encode CP1252/ANSI or UTF-8 ASCII characters into ISO-8859-1 codes --
	function fh.StrEncode_ISO(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_ISO(strText)
		else
			return fh.StrUTF8_ISO(strText)
		end
	end -- function StrEncode_ISO

	-- Convert UTF-8 bytes to a UTF-16 word or pair --
	local tblByte = {}
	local tblLead = { 0x80; 0xC0; 0xE0; 0xF0; 0xF8; 0xFC; }

	function fh.StrUtf8toUtf16(strChar)

		-- Convert any UTF-8 multibytes to UTF-16 --
		local function strUtf8()
			if #tblByte > 0 then
				local intUtf16 = 0
				for intIndex, intByte in ipairs (tblByte) do	-- Convert UTF-8 bytes to UNICODE U+0080 to U+10FFFF
					if intIndex == 1 then
						intUtf16 = intByte - tblLead[#tblByte]
					else
						intUtf16 = intUtf16 * 0x40 + intByte - 0x80
					end
				end
				if intUtf16 > 0xFFFF then							-- U+10000 to U+10FFFF Supplementary Planes -- V2.6
					tblByte = {}
					intUtf16 = intUtf16 - 0x10000
					local intLow10 = 0xDC00 + ( intUtf16 % 0x400 )				-- Low 16-bit Surrogate
					local intTop10 = 0xD800 + math.floor( intUtf16 / 0x400 )	-- High 16-bit Surrogate
					local intChar1 = intTop10 % 0x100
					local intChar2 = math.floor( intTop10 / 0x100 )
					local intChar3 = intLow10 % 0x100
					local intChar4 = math.floor( intLow10 / 0x100 )
					return string.char(intChar1,intChar2,intChar3,intChar4)	-- Surrogate 16-bit Pair
				end
				if intUtf16 < 0xD800								-- U+0080 to U+FFFF (except U+D800 to U+DFFF) -- V2.6
				or intUtf16 > 0xDFFF then							-- Basic Multilingual Plane
					tblByte = {}
					local intChar1 = intUtf16 % 0x100
					local intChar2 = math.floor( intUtf16 / 0x100 )
					return string.char(intChar1,intChar2)		-- BPL 16-bit
				end
				local strUtf8 = ""									-- U+D800 to U+DFFF Reserved Code Points -- V2.6
				for intIndex, intByte in ipairs (tblByte) do
					strUtf8 = strUtf8..string.format("%.2X ",intByte)
				end
				local strUtf16 = string.format("%.4X ",intUtf16)
				fhMessageBox("\n UTF-16 Reserved Code Point U+D800 to U+DFFF \n UTF-16 = "..strUtf16.."  UTF-8 = "..strUtf8.."\n Character will be replaced by a question mark. \n","MB_OK","MB_ICONEXCLAMATION")
				tblByte = {}
				return "?\0"
			end
			return ""
		end -- local function strUtf8

		local intUtf8 = string.byte(strChar)
		if intUtf8 < 0x80 then										-- U+0000 to U+007F (ASCII)
			return strUtf8()..strChar.."\0"						-- Previous UTF-8 multibytes + current ASCII char
		end
		if intUtf8 >= 0xC0 then									-- Next UTF-8 multibyte start
			local strUtf16 = strUtf8()
			table.insert(tblByte,intUtf8)
			return strUtf16											-- Previous UTF-8 multibytes
		end
		table.insert(tblByte,intUtf8)
		return ""
	end -- function StrUtf8toUtf16

	-- Encode UTF-8 bytes into UTF-16 words --
	function fh.StrUTF8_UTF16(strText)
		tblByte = {}													-- (0xFF) flushes last UTF-8 character
		return ( ((strText or "")..string.char(0xFF)):gsub("(.)",fh.StrUtf8toUtf16) )	-- V3.4
	end -- function StrUTF8_UTF16

	-- Encode CP1252/ANSI or UTF-8 characters into UTF-16 words --
	function fh.StrEncode_UTF16(strText)
		if stringx.encoding() == "ANSI" then
			strText = fh.StrANSI_UTF8(strText)
		end
		return fh.StrUTF8_UTF16(strText)
	end -- function StrEncode_UTF16

	local intTop10 = 0

	-- Convert a UTF-16 word or pair to UTF-8 bytes --
	function fh.StrUtf16toUtf8(strChar1,strChar2)
		local intUtf16 = string.byte(strChar2) * 0x100 + string.byte(strChar1)
		if intUtf16 < 0x80 then									-- U+0000 to U+007F (ASCII) 
			return string.char(intUtf16)
		end
		if intUtf16 < 0x800 then									-- U+0080 to U+07FF
			local intByte1 = intUtf16 % 0x40
			intUtf16 = math.floor( intUtf16 / 0x40 )
			local intByte2 = intUtf16
			return string.char( intByte2 + 0xC0, intByte1 + 0x80 )
		end
		if intUtf16 < 0xD800										-- U+0800 to U+FFFF
		or intUtf16 > 0xDFFF then
			local intByte1 = intUtf16 % 0x40
			intUtf16 = math.floor( intUtf16 / 0x40 )
			local intByte2 = intUtf16 % 0x40
			intUtf16 = math.floor( intUtf16 / 0x40 )
			local intByte3 = intUtf16
			return string.char( intByte3 + 0xE0, intByte2 + 0x80, intByte1 + 0x80 )
		end
		if intUtf16 < 0xDC00 then									-- U+10000 to U+10FFFF High 16-bit Surrogate Supplementary Planes -- V2.6
			intTop10 = ( intUtf16 - 0xD800 ) * 0x400 + 0x10000
			return ""
		end
		intUtf16 = intUtf16 - 0xDC00 + intTop10				-- U+10000 to U+10FFFF Low 16-bit Surrogate Supplementary Planes -- V2.6
		local intByte1 = intUtf16 % 0x40
		intUtf16 = math.floor( intUtf16 / 0x40 )
		local intByte2 = intUtf16 % 0x40
		intUtf16 = math.floor( intUtf16 / 0x40 )
		local intByte3 = intUtf16 % 0x40
		intUtf16 = math.floor( intUtf16 / 0x40 )
		local intByte4 = intUtf16
		return string.char( intByte4 + 0xF0, intByte3 + 0x80, intByte2 + 0x80, intByte1 + 0x80 )
	end -- function StrUtf16toUtf8

	-- Encode UTF-16 words into UTF-8 bytes --
	function fh.StrUTF16_UTF8(strText)
		return ( (strText or ""):gsub("(.)(.)",fh.StrUtf16toUtf8) )	-- V3.4
	end -- function StrUTF16_UTF8

	-- Encode UTF-16 words into ANSI characters --
	function fh.StrUTF16_ANSI(strText)
		return fh.StrUTF8_ANSI(fh.StrUTF16_UTF8(strText))
	end -- function StrUTF16_ANSI

	-- Read UTF-16/UTF-8/ANSI file converted to chosen encoding via line iterator --
	local strUtf16 = "^.%z"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strUtf16 = "^.\0"
	end

	function fh.FileLines(strFileName,strEncoding)			-- Derived from http://lua-users.org/wiki/EnhancedFileLines
		local bomUtf16= "^"..string.char(0xFF,0xFE)			-- "ÿþ"
		local bomUtf8 = "^"..string.char(0xEF,0xBB,0xBF)	-- ""
		local fncConv = tostring									-- Function to convert input to current encoding
		local intHead = 1											-- Index to start of current text line
		local intLump = 1024
		local fHandle = general.OpenFile(strFileName,"rb")
		local strText = fHandle:read(1024) or ""				-- Read first lump from file and cater for empty file
		local intBOM = 0
		strEncoding = strEncoding or string.encoding()
		if strText:match(bomUtf16)
		or strText:match(strUtf16) then
			strText,intBOM = strText:gsub(bomUtf16,"") 		-- Strip UTF-16 BOM
			if strEncoding == "ANSI" then						-- Define UTF-16 conversion to current encoding
				fncConv = fh.StrUTF16_ANSI
			else
				fncConv = fh.StrUTF16_UTF8
			end
		elseif strText:match(bomUtf8) then
			strText,intBOM = strText:gsub(bomUtf8,"")			-- Strip UTF-8 BOM
			if strEncoding == "ANSI" then 						-- Define UTF-8 conversion to current encoding
				fncConv = fh.StrUTF8_ANSI
			end
		else
			if strEncoding == "UTF-8" and #strText > 0 then	-- Define ANSI conversion to current encoding and cater for empty file
				fncConv = fh.StrANSI_UTF8
			end
		end
		strText = fncConv(strText)								-- Convert first lump of text

		return function()											-- Iterator function
			local intTail,strTail									-- Index to end of current text line, and terminating characters
			while true do
				intTail, strTail = strText:match("()([\r\n].)",intHead)
				if intTail or not fHandle then
					if intHead > 1 then intLump = 0 end
					break												-- End of line or end of file
				elseif fHandle then
					local strLump = fHandle:read(1024)			-- Read next lump from file
					if strLump then									-- Strip old text and add converted lump
						strText = strText:sub(intHead)..fncConv(strLump)
						intHead = 1
						intLump = 1024
					else
						assert(fHandle:close())					-- End of file
						fHandle = nil
					end
				end
			end
			if not intTail then
				intTail = #strText									-- Last fragment of file
			elseif strTail == "\r\n" then
				intTail = intTail + 1								-- Adjust tail for both \r & \n
			end
			local strLine = strText:sub(intHead,intTail)		-- Extract line from text
			intHead = intTail + 1
			if #strLine > 0 then									-- Return pruned line, tail chars, lump bytes read
				local strBody, strTail = strLine:match("^(.-)([\r\n]+)$")
				return strBody, strTail, intLump
			end    
		end
	end -- function FileLines

	-- Set "[€-ÿ]" ASCII encodings same as Unidecode below
	local tblASCII = { }
	tblASCII["€"] = "=E"
	tblASCII["\129"]="" -- Undefined
	tblASCII["‚"] = ","
	tblASCII["ƒ"] = "f"
	tblASCII["„"] = ",,"
	tblASCII["…"] = "..."
	tblASCII["†"] = "|+"
	tblASCII["‡"] = "|++"
	tblASCII["ˆ"] = "^"
	tblASCII["‰"] = "%0"
	tblASCII["Š"] = "S"
	tblASCII["‹"] = "<"
	tblASCII["Œ"] = "OE"
	tblASCII["\141"]="" -- Undefined
	tblASCII["Ž"] = "Z"
	tblASCII["\143"]="" -- Undefined
	tblASCII["\144"]="" -- Undefined
	tblASCII["‘"] = "'"
	tblASCII["’"] = "'"
	tblASCII["“"] = "\""
	tblASCII["”"] = "\""
	tblASCII["•"] = "*"
	tblASCII["–"] = "-"
	tblASCII["—"] = "--"
	tblASCII["\152"]="~" -- Small Tilde
	tblASCII["™"] = "TM"
	tblASCII["š"] = "s"
	tblASCII["›"] = ">"
	tblASCII["œ"] = "oe"
	tblASCII["\157"]="" -- Undefined
	tblASCII["ž"] = "z"
	tblASCII["Ÿ"] = "Y"
	tblASCII["\160"]=" " -- "&nbsp;" No Break Space
	tblASCII["¡"] = "!"  -- "&iexcl;"
	tblASCII["¢"] = "=c" -- "&cent;"
	tblASCII["£"] = "=L" -- "&pound;"
	tblASCII["¤"] = "=$" -- "&curren;"
	tblASCII["¥"] = "=Y" -- "&yen;"
	tblASCII["¦"] = "|"
	tblASCII["§"] = "=SS"
	tblASCII["¨"] = "\""
	tblASCII["©"] = "(C)"
	tblASCII["ª"] = "a"
	tblASCII["«"] = "<<"
	tblASCII["¬"] = "-"
	tblASCII["­"] = "-"  -- "&shy;" Soft Hyphen
	tblASCII["®"] = "(R)"
	tblASCII["¯"] = "-"
	tblASCII["°"] = "=o"
	tblASCII["±"] = "+-"
	tblASCII["²"] = "2"
	tblASCII["³"] = "3"
	tblASCII["´"] = "'"
	tblASCII["µ"] = "=u"
	tblASCII["¶"] = "=p"
	tblASCII["·"] = "*"
	tblASCII["¸"] = ","
	tblASCII["¹"] = "1"
	tblASCII["º"] = "o"
	tblASCII["»"] = ">>"
	tblASCII["¼"] = "1/4"
	tblASCII["½"] = "1/2"
	tblASCII["¾"] = "3/4"
	tblASCII["¿"] = "?"
	tblASCII["À"] = "A"
	tblASCII["Á"] = "A"
	tblASCII["Â"] = "A"
	tblASCII["Ã"] = "A"
	tblASCII["Ä"] = "A"
	tblASCII["Å"] = "A"
	tblASCII["Æ"] = "AE"
	tblASCII["Ç"] = "C"
	tblASCII["È"] = "E"
	tblASCII["É"] = "E"
	tblASCII["Ê"] = "E"
	tblASCII["Ë"] = "E"
	tblASCII["Ì"] = "I"
	tblASCII["Í"] = "I"
	tblASCII["Î"] = "I"
	tblASCII["Ï"] = "I"
	tblASCII["Ð"] = "D"
	tblASCII["Ñ"] = "N"
	tblASCII["Ò"] = "O"
	tblASCII["Ó"] = "O"
	tblASCII["Ô"] = "O"
	tblASCII["Õ"] = "O"
	tblASCII["Ö"] = "O"
	tblASCII["×"] = "*"
	tblASCII["Ø"] = "O"
	tblASCII["Ù"] = "U"
	tblASCII["Ú"] = "U"
	tblASCII["Û"] = "U"
	tblASCII["Ü"] = "U"
	tblASCII["Ý"] = "Y"
	tblASCII["Þ"] = "TH"
	tblASCII["ß"] = "ss"
	tblASCII["à"] = "a"
	tblASCII["á"] = "a"
	tblASCII["â"] = "a"
	tblASCII["ã"] = "a"
	tblASCII["ä"] = "a"
	tblASCII["å"] = "a"
	tblASCII["æ"] = "ae"
	tblASCII["ç"] = "c"
	tblASCII["è"] = "e"
	tblASCII["é"] = "e"
	tblASCII["ê"] = "e"
	tblASCII["ë"] = "e"
	tblASCII["ì"] = "i"
	tblASCII["í"] = "i"
	tblASCII["î"] = "i"
	tblASCII["ï"] = "i"
	tblASCII["ð"] = "d"
	tblASCII["ñ"] = "n"
	tblASCII["ò"] = "o"
	tblASCII["ó"] = "o"
	tblASCII["ô"] = "o"
	tblASCII["õ"] = "o"
	tblASCII["ö"] = "o"
	tblASCII["÷"] = "/"
	tblASCII["ø"] = "o"
	tblASCII["ù"] = "u"
	tblASCII["ú"] = "u"
	tblASCII["û"] = "u"
	tblASCII["ü"] = "u"
	tblASCII["ý"] = "y"
	tblASCII["þ"] = "th"
	tblASCII["ÿ"] = "y"

	-- Encode CP1252/ANSI characters into ASCII codes [\000-\127] --
	function fh.StrANSI_ASCII(strText)
		return strEncode(strText,"[€-ÿ]",tblASCII)
	end -- function StrANSI_ASCII

	--[=[
		Unidecode converts each codepoint into a few ASCII characters.
		Lookup table indexed by codepoint [0x0000]-[0xFFFF] gives an ASCII string.
		i.e. strASCII = Unidecode[intByte2][intByte1] or "=?" allowing for partially populated table.
		See http://search.cpan.org/dist/Text-Unidecode/ and follow Browse to:
		See http://cpansearch.perl.org/src/SBURKE/Text-Unidecode-1.22/lib/Text/Unidecode/
		where each x??.pm gives 256 ASCII conversions.
		Start with the first few European accented characters, and add the others later.
	--]=]

	local Unidecode = { }

	function fh.StrUnidecode(strChar1,strChar2)				-- Decode UTF-16 byte pair into ASCII characters
		return Unidecode[string.byte(strChar2)][string.byte(strChar1)] or "=?"
	end  -- function StrUnidecode

	-- Encode UTF-8 characters into ASCII codes [\000-\126] --
	function fh.StrUTF8_ASCII(strText)
		strText = fh.StrUTF8_UTF16(strText)						-- Convert to UTF-16 Unicode and then to ASCII
		return ( strText:gsub("(.)(.)",fh.StrUnidecode) )
	end -- function StrUTF8_ASCII

	-- Encode CP1252/ANSI or UTF-8 into ASCII codes [\000-\126] --
	function fh.StrEncode_ASCII(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_ASCII(strText)
		else
			return fh.StrUTF8_ASCII(strText)
		end
	end -- function StrEncode_ASCII

	-- Set markup language break tag --
	function fh.SetBreakTag(br_New)
		if not (br_New or ""):match(br_Lua) then				-- Ensure new break tag is "<br>" or "<br >" or "<br/>" or "<br />"
			br_New = "<br />"
		end
		br_Tag = br_New
	end -- function SetBreakTag

	for intByte = 0x00, 0xFF do Unidecode[intByte] = { } end

	Unidecode[0x00] =
	{[0]="\00";"\01";"\02";"\03";"\04";"\05";"\06";"\a";"\b";"\t";"\n";"\v";"\f";"\r";"\14";"\15";"\16";"\17";"\18";"\19";"\20";"\21";"\22";"\23";"\24";"\25";"\26";"\27";"\28";"\29";"\30";"\31";
		" ";"!";'"';"#";"$";"%";"&";"'";"(";")";"*";"+";",";"-";".";"/";"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";":";";";"<";"=";">";"?";							-- 0x20 to 0x3F
		"@";"A";"B";"C";"D";"E";"F";"G";"H";"I";"J";"K";"L";"M";"N";"O";"P";"Q";"R";"S";"T";"U";"V";"W";"X";"Y";"Z";"[";"\\";"]";"^";"_";							-- 0x40 to 0x5F
		"`";"a";"b";"c";"d";"e";"f";"g";"h";"i";"j";"k";"l";"m";"n";"o";"p";"q";"r";"s";"t";"u";"v";"w";"x";"y";"z";"{";"|";"}";"~";"\127";							-- 0x60 to 0x7F
		""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; "";								-- 0x80 to 0x9F
		" ";"!";"=c";"=L";"=$";"=Y";"|";"=SS";'"';"(C)";"a";"<<";"-";"-";"(R)";"-";"=o";"+-";"2";"3";"'";"=u";"=P";"*";",";"1";"o";">>";"1/4";"1/2";"3/4";"?";	-- 0xA0 to 0xBF
		"A";"A";"A";"A";"A";"A";"AE";"C";"E";"E";"E";"E";"I";"I";"I";"I";"D";"N";"O";"O";"O";"O";"O";"*";"O";"U";"U";"U";"U";"Y";"TH";"ss"; 						-- 0xC0 to 0xDF
		"a";"a";"a";"a";"a";"a";"ae";"c";"e";"e";"e";"e";"i";"i";"i";"i";"d";"n";"o";"o";"o";"o";"o";"/";"o";"u";"u";"u";"u";"y";"th";"y";							-- 0xE0 to 0xFF
	}
	Unidecode[0x01] =
	{[0]="A";"a";"A";"a";"A";"a";"C";"c";"C";"c";"C";"c";"C";"c";"D";"d";"D";"d";"E";"e";"E";"e";"E";"e";"E";"e";"E";"e";"G";"g";"G";"g";							-- 0x00 to 0x1F
		"G";"g";"G";"g";"H";"h";"H";"h";"I";"i";"I";"i";"I";"i";"I";"i";"I";"i";"IJ";"ij";"J";"j";"K";"k";"k";"L";"l";"L";"l";"L";"l";"L";							-- 0x20 to 0x3F
		"l";"L";"l";"N";"n";"N";"n";"N";"n";"'n";"ng";"NG";"O";"o";"O";"o";"O";"o";"OE";"oe";"R";"r";"R";"r";"R";"r";"S";"s";"S";"s";"S";"s";						-- 0x40 to 0x5F
		"S";"s";"T";"t";"T";"t";"T";"t";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"W";"w";"Y";"y";"Y";"Z";"z";"Z";"z";"Z";"z";"s";							-- 0x60 to 0x7F
		"b";"B";"B";"b";"6";"6";"O";"C";"c";"D";"D";"D";"d";"d";"3";"@";"E";"F";"f";"G";"G";"hv";"I";"I";"K";"k";"l";"l";"W";"N";"n";"O";							-- 0x80 to 0x9F
		"O";"o";"OI";"oi";"P";"p";"YR";"2";"2";"SH";"sh";"t";"T";"t";"T";"U";"u";"Y";"V";"Y";"y";"Z";"z";"ZH";"ZH";"zh";"zh";"2";"5";"5";"ts";"w";				-- 0xA0 to 0xBF
		"|";"||";"|=";"!";"DZ";"Dz";"dz";"LJ";"Lj";"lj";"NJ";"Nj";"nj";"A";"a";"I";"i";"O";"o";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"@";"A";"a";				-- 0xC0 to 0xDF
		"A";"a";"AE";"ae";"G";"g";"G";"g";"K";"k";"O";"o";"O";"o";"ZH";"zh";"j";"DZ";"Dz";"dz";"G";"g";"HV";"W";"N";"n";"A";"a";"AE";"ae";"O";"o";				-- 0xE0 to 0xFF
	}
	Unidecode[0x02] =
	{[0]="A";"a";"A";"a";"E";"e";"E";"e";"I";"i";"I";"i";"O";"o";"O";"o";"R";"r";"R";"r";"U";"u";"U";"u";"S";"s";"T";"t";"Y";"y";"H";"h";							-- 0x00 to 0x1F
		"N";"d";"OU";"ou";"Z";"z";"A";"a";"E";"e";"O";"o";"O";"o";"O";"o";"O";"o";"Y";"y";"l";"n";"t";"j";"db";"qp";"A";"C";"c";"L";"T";"s";						-- 0x20 to 0x3F
		"z";"[?]";"[?]";"B";"U";"^";"E";"e";"J";"j";"q";"q";"R";"r";"Y";"y";"a";"a";"a";"b";"o";"c";"d";"d";"e";"@";"@";"e";"e";"e";"e";"j";						-- 0x40 to 0x5F
		"g";"g";"g";"g";"u";"Y";"h";"h";"i";"i";"I";"l";"l";"l";"lZ";"W";"W";"m";"n";"n";"n";"o";"OE";"O";"F";"r";"r";"r";"r";"r";"r";"r";							-- 0x60 to 0x7F
		"R";"R";"s";"S";"j";"S";"S";"t";"t";"u";"U";"v";"^";"w";"y";"Y";"z";"z";"Z";"Z";"?";"?";"?";"C";"@";"B";"E";"G";"H";"j";"k";"L";							-- 0x80 to 0x9F
		"q";"?";"?";"dz";"dZ";"dz";"ts";"tS";"tC";"fN";"ls";"lz";"WW";"]]";"h";"h";"h";"h";"j";"r";"r";"r";"r";"w";"y";"'";'"';"`";"'";"`";"`";"'";				-- 0xA0 to 0xBF
		"?";"?";"<";">";"^";"V";"^";"V";"'";"-";"/";"\\";",";"_";"\\";"/";":";".";"`";"'";"^";"V";"+";"-";"V";".";"@";",";"~";'"';"R";"X";							-- 0xC0 to 0xDF
		"G";"l";"s";"x";"?";"";"";"";"";"";"";"";"V";"=";'"';"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";	-- 0xE0 to 0xFF
	}
	Unidecode[0x03] =
	{
	}
	Unidecode[0x04] =
	{
	}
	Unidecode[0x20] =
	{[0]=" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";"";"";"";"";"-";"-";"-";"-";"--";"--";"||";"_";"'";"'";",";"'";'"';'"';",,";'"';																-- 0x00 to 0x1F
		"|+";"|++";"*";"*>";".";"..";"...";".";"\n";"\n\n";"";"";"";"";"";" ";"%0";"%00";"'";"''";"'''";"`";"``";"```";"^";"<";">";"*";"!!";"!?";"-";"_";											-- 0x20 to 0x3F
		"-";"^";"***";"--";"/";"-[";"]-";"[?]";"?!";"!?";"7";"PP";"(]";"[)";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";	-- 0x40 to 0x5F
		"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"0";"";"";"";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"n";													-- 0x60 to 0x7F
		"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";						-- 0x80 to 0x9F
		"ECU";"CL";"Cr";"FF";"L";"mil";"N";"Pts";"Rs";"W";"NS";"D";"=E";"K";"T";"Dr";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";		-- 0xA0 to 0xBF
		"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";											-- 0xC0 to 0xDF
		"";"";"";"";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";		-- 0xE0 to 0xFF
	}
	Unidecode[0x21] =
	{[34]="TM";
	}

	return fh

end -- local function encoder_v3

local encoder = encoder_v3()										-- To access FH encoder chars module

--[[
@Module:			+fh+progbar_v3
@Author:			Mike Tate
@Version:			3.1
@LastUpdated:		23 Jan 2026
@Description:		Progress Bar library module.
@V3.1:				Use NATIVEPARENT amd CENTERPARENT.
@V3.0:				Function Prototype Closure version.
@V1.0:				Initial version.
]]

local function progbar_v3()

	local fh = {}														-- Local environment table

	require "iuplua"													-- To access GUI window builder

	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")					-- Needed for IUP 3.28

	local tblBars = {}												-- Table for optional external attributes
	local strBack = "255 255 255"									-- Background colour default is white
	local strBody = "0 0 0"											-- Body text colour default is black
	local strFont = nil												-- Font dialogue default is current font
	local strStop = "255 0 0"										-- Stop button colour default is red
	local intPosX = iup.CENTERPARENT								-- Show window default position is central	-- V3.1
	local intPosY = iup.CENTERPARENT
	local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
	local lblText, barGauge, lblDelta, btnStop, dlgGauge

	local function doFocus()										-- Bring the Progress Bar window into Focus
		dlgGauge.BringFront="YES"									-- If used too often, inhibits other windows scroll bars, etc
	end -- local function doFocus

	local function doUpdate()										-- Update the Progress Gauge and the Delta % with clock
		barGauge.Value = intVal
		lblDelta.Title = string.format("%4d %%      %s ",math.floor(intPercent),strClock)
	end -- local function doUpdate

	local function doReset()										-- Reset all dialogue variables and Update display
		intVal		= 0													-- Current value of Progress Bar
		intPercent= 0.01												-- Percentage of progress
		intStart	= os.time()										-- Start time of progress
		intDelta	= 0													-- Delta time of progress
		intScale	= math.ceil( intMax / 1000 )					-- Scale of percentage per second of progress (initial guess is corrected in Step function)
		strClock	= "00 : 00 : 00"									-- Clock delta time display
		isBarStop	= false											-- Stop button pressed signal
		doUpdate()
		doFocus()
	end -- local function doReset

	function fh.Start(strTitle,intMaximum)						-- Create & start Progress Bar window
		if not dlgGauge then
			strTitle	= strTitle or ""								-- Dialogue and button title
			intMax		= intMaximum or 100							-- Maximun range of Progress Bar, default is 100
			local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30"			-- Adjust Stop button size to Title
			lblText	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
			barGauge	= iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
			lblDelta	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
			btnStop	= iup.button	{ Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; }	-- Signal Stop button pressed	return iup.CLOSE -- Often caused main GUI to close !!!
			dlgGauge	= iup.dialog	{ Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES";	-- Remove Windows minimize/maximize menu
								iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
									lblText;
									barGauge;
									lblDelta;
									btnStop;
								};
								move_cb	= function(self,x,y) tblBars.X = x tblBars.Y = y end;
								close_cb	= btnStop.action;		-- Windows Close button = Stop button
							}
			if type(tblBars.GUI) == "table"
			and type(tblBars.GUI.ShowDialogue) == "function" then
				dlgGauge.move_cb = nil								-- Use GUI library to show & move window
				tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
			else
				if fhGetAppVersion() > 6 then 					-- Window centres on FH parent	-- V3.1
					iup.SetAttribute(dlgGauge,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
				end
				dlgGauge:showxy(intPosX,intPosY)					-- Show the Progress Bar window
			end
			doReset()													-- Reset the Progress Bar display
		end
	end -- function Start

	function fh.Message(strText)									-- Show the Progress Bar message
		if dlgGauge then lblText.Title = strText end
	end -- function Message

	function fh.Step(intStep)										-- Step the Progress Bar forward
		if dlgGauge then
			intVal = intVal + ( intStep or 1 )					-- Default step is 1
			local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
			if intPercent ~= intNew then							-- Update progress once per percent or per second, whichever is smaller
				intPercent = math.max( 0.1, intNew )				-- Ensure percentage is greater than zero
				if intVal > intMax then intVal = intMax intPercent = 100 end		-- Ensure values do not exceed maximum
				intNew = os.difftime(os.time(),intStart)
				if intDelta < intNew then							-- Update clock of elapsed time
					intDelta = intNew
					intScale = math.ceil( intDelta / intPercent )	-- Scale of seconds per percentage step
					local intHour = math.floor( intDelta / 3600 )
					local intMins = math.floor( intDelta / 60 - intHour * 60 )
					local intSecs = intDelta - intMins * 60 - intHour * 3600
					strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
				end
				doUpdate()											-- Update the Progress Bar display
			end
			iup.LoopStep()
		end
	end -- function Step

	function fh.Focus()												-- Bring the Progress Bar window to front
		if dlgGauge then doFocus() end
	end -- function Focus

	function fh.Reset()												-- Reset the Progress Bar display
		if dlgGauge then doReset() end
	end -- function Reset

	function fh.Stop()												-- Check if Stop button pressed
		iup.LoopStep()
		return isBarStop
	end -- function Stop

	function fh.Close()												-- Close the Progress Bar window
		isBarStop = false
		if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
	end -- function Close

	function fh.Setup(tblSetup)									-- Setup optional table of external attributes
		if tblSetup then
			tblBars = tblSetup
			strBack = tblBars.Back or strBack						-- Background colour
			strBody = tblBars.Body or strBody						-- Body text colour
			strFont = tblBars.Font or strFont						-- Font dialogue
			strStop = tblBars.Stop or strStop						-- Stop button colour
			intPosX = tblBars.X or intPosX						-- Window position
			intPosY = tblBars.Y or intPosY
		end
	end -- function Setup

	return fh

end -- local function progbar_v3

local progbar = progbar_v3()										-- To access FH progress bars module

--[[
@Module:			+fh+iup_gui_v3
@Author:			Mike Tate
@Version:			4.6
@LastUpdated:		18 Feb 2026
@Description:		Graphical User Interface Library Module
@V4.6:				Improve handling of centred window RasterSize in fh.ShowDialogue(...);
@V4.5:				Adjust CheckVersionInStore() for dedicated button use;
@V4.4:				Introduce use of NATIVEPARENT and CENTERPARENT to centre on parent window by default; Ensure not off screen; Monitors with -ve X;
@V4.3:				Added memo options to CheckVersionInStore;
@V4.2:				Skip if standalone GEDCOM in fh.SaveSettings() and getDataFiles();
@V4.1:				CheckVersionInStore() save & retrieve latest version in file; Remove old wiki Help features;
@V4.0:				Cater for full UTF-8 filenames;
@V3.9:				ShowDialogue() popup closure fhSleep() added; CheckVersionInStore() at monthly intervals;
@V3.8:				Function Prototype Closure version.
@V3.7:				AssignAttributes(tblControls) now allows any string attribute to invoke a function.
@V3.6:				anyMemoDialogue() sets TopMost attribute.
@V3.5:				Replace IsNormalWindow(iupDialog) with SetWindowCoord(tblName) and update CheckWindowPosition(tblName) to prevent negative values freezing main dialog.
@V3.4:				Use general.MakeFolder() to ensure key folders exist, add Get/PutRegKey(), check Registry IE Shell Version in HelpDialogue(), better error handling in LoadSettings().
@V3.3:				LoadFolder() and SaveFolder() use global folder as default for local folder to improve synch across PC.
@V3.2:				Load & Save settings now use a single clipboard so Local PC settings are preserved across synchronised PC.
@V3.1:				IUP 3.11.2 iup.GetGlobal("VERSION") to top, HelpDialogue conditional ExpandChildren="YES/NO", RefreshDialogue uses NaturalSize, SetUtf8Mode(), Load/SaveFolder(), etc
@V3.0:				ShowDialogue "dialog" mode for Memo, new DestroyDialogue, NewHelpDialogue tblAttr for Font, AssignAttributes intSkip, CustomDialogue iup.CENTERPARENT+, IUP Workaround, BalloonToggle, Initialise test Plugin file exists.
@V2.0:				Support for Plugin Data scope, new FontDialogue, RefreshDialogue, AssignAttributes, httpRequest handler, keep "dialog" mode.
@V1.0:				Initial version.
]]

local function iup_gui_v3()

	local fh = {}																			-- Local environment table

	require "iuplua"																		-- To access GUI window builder
	require "iupluacontrols"															-- To access GUI window controls
	require "lfs"																			-- To access LUA filing system
	require "iupluaole"																	-- To access OLE subsystem
	require "luacom"																		-- To access COM subsystem

	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")										-- Needed for IUP 3.28

	local iupVersion = iup.GetGlobal("VERSION")										-- Obtain IUP module version

	-- "iuplua" Omitted Constants Workaround --

	iup.TOP    = iup.LEFT
	iup.BOTTOM = iup.RIGHT
	iup.RED    = iup.RGB(1,0,0)
	iup.GREEN  = iup.RGB(0,1,0)
	iup.BLUE   = iup.RGB(0,0,1)
	iup.BLACK  = iup.RGB(0,0,0)
	iup.WHITE  = iup.RGB(1,1,1)
	iup.YELLOW = iup.RGB(1,1,0)

	-- Shared Interface Attributes & Functions --

	fh.Version	= " "																	-- Plugin Version
	fh.History	= fh.Version															-- Version History

	fh.Red			= "255 0 0"															-- Color attributes (must exclude leading zeros & spaces to allow value comparisons)
	fh.Maroon		= "128 0 0"
	fh.Amber		= "250 160 0"
	fh.Orange		= "255 165 0"
	fh.Yellow		= "255 255 0"
	fh.Olive		= "128 128 0"
	fh.Lime		= "0 255 0"
	fh.Green		= "0 128 0"
	fh.Cyan		= "0 255 255"
	fh.Teal		= "0 128 128"
	fh.Blue		= "0 0 255"
	fh.Navy		= "0 0 128"
	fh.Magenta	= "255 0 255"
	fh.Purple		= "128 0 128"
	fh.Black		= "0 0 0"
	fh.Gray		= "128 128 128"
	fh.Silver		= "192 192 192"
	fh.Smoke		= "240 240 240"
	fh.White		= "255 255 255"
	fh.Risk		= fh.Red																-- Risk colour for hazardous controls such as Close/Delete buttons
	fh.Warn		= fh.Magenta															-- Warn colour for caution controls and warnings
	fh.Safe		= fh.Green															-- Safe colour for active controls such as most buttons
	fh.Info		= fh.Black															-- Info colour for text controls such as labels/tabs
	fh.Head		= fh.Black															-- Head colour for headings
	fh.Body		= fh.Black															-- Body colour for body text
	fh.Back		= fh.White															-- Background colour for all windows

	fh.Gap			= "8"																	-- Layout attributes Gap was "10"
	fh.Border		= "8x8"																-- 	was BigMargin="10x10"
	fh.Margin		= "1x1"																--	was MinMargin
	fh.Balloon	= "NO"																	-- Tooltip balloon mode

	fh.FontSet	=	0																	-- Legacy GUI font set assigned by FontAssignment but used globally
	fh.FontHead	= ""
	fh.FontBody	= ""

	local GUI = { }																		-- Sub-table for GUI Dialogue attributes to allow any "Name"

	--[[
	GUI.Name			table of dialogue attributes, where Name is Font, Main, Memo, Bars, etc
	GUI.Name.CoordX	x co-ordinate	( Loaded & Saved by default )
	GUI.Name.CoordY	y co-ordinate	( Loaded & Saved by default )
	GUI.Name.Dialog	dialogue handle
	GUI.Name.Focus	focus button handle
	GUI.Name.Frame	dialogframe mode, "normal" = dialogframe="NO" else "YES", "showxy" = showxy(), "popup" or "keep" = popup(), default is "normal & showxy"
	GUI.Name.Height	height
	GUI.Name.Raster	rastersize		( Loaded & Saved by default )
	GUI.Name.Width	width
	GUI.Name.Back		ProgressBar background colour
	GUI.Name.Body		ProgressBar body text colour
	GUI.Name.Font		ProgressBar font style
	GUI.Name.Stop		ProgressBar Stop button colour
	GUI.Name.GUI		Module table usable by other modules e.g. progbar.Setup
	--]]

	-- tblScrn[1] = origin x, tblScrn[2] = origin y, tblScrn[3] = width, tblScrn[4] = height
	local tblScrn = stringx.splitnumbers(iup.GetGlobal("VIRTUALSCREEN"))		-- Used by CustomDialogue() and CheckWindowPosition() and ShowDialogue() below
	local intMinX = tblScrn[1]
	local intMinY = tblScrn[2]															-- V4.4
	local intMaxW = tblScrn[3]
	local intMaxH = tblScrn[4]

	function fh.BalloonToggle()														-- Toggle tooltips Balloon mode
		local tblToggle = { YES="NO"; NO="YES"; }
		fh.Balloon = tblToggle[fh.Balloon]
		fh.SaveSettings()
	end -- function BalloonToggle

	iup.SetGlobal("UTF8MODE","NO")
	iup.SetGlobal("UTF8MODE_FILE","NO") 												-- V4.0
	function fh.SetUtf8Mode()															-- Set IUP into UTF-8 mode
		if iupVersion == "3.5" or stringx.encoding() == "ANSI" then return false end
		iup.SetGlobal("UTF8MODE","YES")
		iup.SetGlobal("UTF8MODE_FILE","YES")											-- V4.0
		return true
	end -- function SetUtf8Mode

	local function tblOfNames(...)													-- Get table of dialogue Names including "Font","Main" by default	-- V4.4
		local arg = {...}
		local tblNames = {"Font";"Main";}
		for intName, strName in ipairs(arg) do
			if type(strName) == "string"
			and strName ~= "Font"
			and strName ~= "Main" then
				table.insert(tblNames,strName)
			end
		end
		return tblNames
	end -- local function tblOfNames

	local function tblNameFor(strName)												-- Get table of parameters for chosen dialogue Name
		strName = tostring(strName)
		if not GUI[strName] then														-- Need new table with default minimum & raster size, and X & Y co-ordinates
			GUI[strName] = { }
			local tblName = GUI[strName]
			tblName.Raster = "x"
			tblName.CoordX = iup.CENTERPARENT
			tblName.CoordY = iup.CENTERPARENT
		end
		return GUI[strName]
	end -- local function tblNameFor

	local function intDimension(intMin,intVal,intMax)								-- Return a number bounded by intMin and intMax
		if not intVal then return 0 end												-- Except if no value then return 0
		intVal = tonumber(intVal) or (intMin+intMax)/2
		return math.max(intMin,math.min(intVal,intMax))
	end -- local function intDimension

	function fh.CustomDialogue(strName,strRas,intX,intY)							-- GUI custom window raster size, and X & Y co-ordinates
		-- strRas  nil = old size, "x" or "0x0" = min size, "999x999" = new size
		-- intX/Y  nil = central, "99" = co-ordinate position
		local tblName = tblNameFor(strName)
		local tblSize = {}
		local intWide = 0
		local intHigh = 0
		strRas = strRas or tblName.Raster
		if strRas then																	-- Ensure raster size is between minimum and screen size
			tblSize = stringx.splitnumbers(strRas)
			intWide = intDimension(intWide,tblSize[1],intMaxW)
			intHigh = intDimension(intHigh,tblSize[2],intMaxH)
			strRas = tostring(intWide.."x"..intHigh)
		end
		if intX and intX < iup.CENTERPARENT then
			intX = intDimension(intMinX,intX,intMaxW-intWide)						-- Ensure X co-ordinate positions window on screen	-- V4.4
		end
		if intY and intY < iup.CENTERPARENT then
			intY = intDimension(intMinY,intY,intMaxH-intHigh)						-- Ensure Y co-ordinate positions window on screen	-- V4.4
		end
		tblName.Raster  = strRas or "x"
		tblName.CoordX  = tonumber(intX) or iup.CENTERPARENT						-- V4.4
		tblName.CoordY  = tonumber(intY) or iup.CENTERPARENT						-- V4.4
	end -- function CustomDialogue

	function fh.DefaultDialogue(...)													-- GUI default window minimum & raster size, and X & Y co-ordinates
		for intName, strName in ipairs(tblOfNames(...)) do
			fh.CustomDialogue(strName)
		end
	end -- function DefaultDialogue

	function fh.DialogueAttributes(strName)											-- Provide named Dialogue Attributes
		local tblName = tblNameFor(strName)											-- tblName.Dialog = dialog handle, so any other attributes could be retrieved
		local tblSize = stringx.splitnumbers(tblName.Raster or "x")				-- Split Raster Size into width=tblSize[1] and height=tblSize[2]
		tblName.Width = tblSize[1]
		tblName.Height= tblSize[2]
		tblName.Back = fh.Back															-- Following only needed for NewProgressBar
		tblName.Body = fh.Body
		tblName.Font = fh.FontBody
		tblName.Stop = fh.Risk
		tblName.GUI  = fh																-- Module table
		return tblName
	end -- function DialogueAttributes

	local strDefaultScope = "Project"													-- Default scope for Load/Save data is per Project/User/Machine as set by PluginDataScope()

	local tblClipProj = { }
	local tblClipUser = { }																-- Clipboards of sticky data for each Plugin Data scope -- V3.2
	local tblClipMach = { }

	local function doLoadData(strParam,strDefault,strScope)						-- Load sticky data for Plugin Data scope
		strScope = tostring(strScope or strDefaultScope):lower()
		local tblClipData = tblClipProj
		if     strScope:match("user") then tblClipData = tblClipUser
		elseif strScope:match("mach") then tblClipData = tblClipMach
		end
		return tblClipData[strParam] or strDefault
	end -- local function doLoadData

	function fh.LoadGlobal(strParam,strDefault,strScope)							-- Load Global Parameter for all PC
		return doLoadData(strParam,strDefault,strScope)
	end -- function LoadGlobal

	function fh.LoadLocal(strParam,strDefault,strScope) 							-- Load Local Parameter for this PC
		return doLoadData(fh.ComputerName.."-"..strParam,strDefault,strScope)
	end -- function LoadLocal

	local function doLoadFolder(strFolder)											-- Use relative paths to let Paths change -- V3.3
		strFolder = strFolder:gsub("^FhDataPath",function() return fh.FhDataPath end)	-- Full path to .fh_data folder
		strFolder = strFolder:gsub("^PublicPath",function() return fh.PublicPath end)	-- Full path to Public folder
		strFolder = strFolder:gsub("^FhProjPath",function() return fh.FhProjPath end)	-- Full path to project folder
		return strFolder
	end -- local function doLoadFolder

	function fh.LoadFolder(strParam,strDefault,strScope)							-- Load Folder Parameter for this PC -- V3.3
		local strFolder = doLoadFolder(fh.LoadLocal(strParam,"",strScope))
		if not general.FlgFolderExists(strFolder) then								-- If no local folder try global folder
			strFolder = doLoadFolder(fh.LoadGlobal(strParam,strDefault,strScope))
		end
		return strFolder
	end -- function LoadFolder

	function fh.LoadDialogue(...)														-- Load Dialogue Parameters for "Font","Main" by default
		for intName, strName in ipairs(tblOfNames(...)) do
			local tblName = tblNameFor(strName)
			tblName.Raster = tostring(fh.LoadLocal(strName.."R",tblName.Raster))
			tblName.CoordX = tonumber(fh.LoadLocal(strName.."X",tblName.CoordX))
			tblName.CoordY = tonumber(fh.LoadLocal(strName.."Y",tblName.CoordY))
			fh.CheckWindowPosition(tblName)
		end
	end -- function LoadDialogue

	function fh.LoadSettings(...)														-- Load Sticky Settings from File
		for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
			strFileName = fh[strFileName]
			if general.FlgFileExists(strFileName) then								-- Load Settings File in table lines with key & val fields
				local tblField = {}
				local strClip = general.StrLoadFromFile(strFileName)				-- V4.0
				for strLine in strClip:gmatch("[^\r\n]+") do						-- V4.0
					if #tblField == 0
					and strLine:match("^return {")									-- Unless entire Sticky Data table was saved
					and type(table.load) == "function" then
						local tblClip, strErr = table.load(strFileName)			-- Load Settings File table
						if strErr then error(strErr.."\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..strFileName.."\n\nError detected.") end
						for i,j in pairs (tblClip) do
							tblClipData[i] = tblClip[i]
						end
						break
					end
					tblField = stringx.split(strLine,"=")
					if tblField[1] then tblClipData[tblField[1]] = tblField[2] end
				end
			else
				for i,j in pairs (tblClipData) do
					tblClipData[i] = nil												-- Restore defaults and clear any junk		-- V4.0
				end
			end
		end
		fh.Safe    = tostring(fh.LoadGlobal("SafeColor",fh.Safe))
		fh.Warn    = tostring(fh.LoadGlobal("WarnColor",fh.Warn))
		fh.Risk    = tostring(fh.LoadGlobal("RiskColor",fh.Risk))
		fh.Head    = tostring(fh.LoadGlobal("HeadColor",fh.Head))
		fh.Body    = tostring(fh.LoadGlobal("BodyColor",fh.Body))
		fh.FontHead= tostring(fh.LoadGlobal("FontHead" ,fh.FontHead))
		fh.FontBody= tostring(fh.LoadGlobal("FontBody" ,fh.FontBody))
		fh.History = tostring(fh.LoadGlobal("History"  ,fh.History))
		fh.Balloon = tostring(fh.LoadGlobal("Balloon"  ,fh.Balloon, "Machine"))
		fh.LoadDialogue(...)
	end -- function LoadSettings

	local function doSaveData(strParam,anyValue,strScope)							-- Save sticky data for Plugin Data scope
		strScope = tostring(strScope or strDefaultScope):lower()
		local tblClipData = tblClipProj
		if     strScope:match("user") then tblClipData = tblClipUser
		elseif strScope:match("mach") then tblClipData = tblClipMach
		end
		tblClipData[strParam] = anyValue
	end -- local function doSaveData

	function fh.SaveGlobal(strParam,anyValue,strScope)							-- Save Global Parameter for all PC
		doSaveData(strParam,anyValue,strScope)
	end -- function SaveGlobal

	function fh.SaveLocal(strParam,anyValue,strScope)								-- Save Local Parameter for this PC
		doSaveData(fh.ComputerName.."-"..strParam,anyValue,strScope)
	end -- function SaveLocal

	function fh.SaveFolder(strParam,strFolder,strScope)							-- Save Folder Parameter for this PC
		strFolder = stringx.replace(strFolder,fh.FhDataPath,"FhDataPath")		-- Full path to .fh_data folder
		strFolder = stringx.replace(strFolder,fh.PublicPath,"PublicPath")		-- Full path to Public folder
		strFolder = stringx.replace(strFolder,fh.FhProjPath,"FhProjPath")		-- Full path to project folder
		fh.SaveGlobal(strParam,strFolder,strScope)									-- V3.3
		fh.SaveLocal(strParam,strFolder,strScope)									-- Uses relative paths to let Paths change
	end -- function SaveFolder

	function fh.SaveDialogue(...)														-- Save Dialogue Parameters for "Font","Main" by default
		for intName, strName in ipairs(tblOfNames(...)) do
			local tblName = tblNameFor(strName)
			fh.SaveLocal(strName.."R",tblName.Raster)
			fh.SaveLocal(strName.."X",tblName.CoordX)
			fh.SaveLocal(strName.."Y",tblName.CoordY)
		end
	end -- function SaveDialogue

	function fh.SaveSettings(...)														-- Save Sticky Settings to File
		fh.SaveDialogue(...)
		fh.SaveGlobal("SafeColor",fh.Safe)
		fh.SaveGlobal("WarnColor",fh.Warn)
		fh.SaveGlobal("RiskColor",fh.Risk)
		fh.SaveGlobal("HeadColor",fh.Head)
		fh.SaveGlobal("BodyColor",fh.Body)
		fh.SaveGlobal("FontHead" ,fh.FontHead)
		fh.SaveGlobal("FontBody" ,fh.FontBody)
		fh.SaveGlobal("History"  ,fh.History)
		fh.SaveGlobal("Balloon"  ,fh.Balloon, "Machine")
		for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
			for i,j in pairs (tblClipData) do											-- Check if table has any entries
				strFileName = fh[strFileName]
				if #strFileName > 0 then												-- Skip for standalone GEDCOM -- V4.2
					if type(table.save) == "function" then							-- Save entire Settings File table per Project/User/Machine
						table.save(tblClipData,strFileName)
					else
						local tblClip = {}
						for strKey,strVal in pairs(tblClipData) do					-- Else save Settings File lines with key & val fields		-- V4.0
							table.insert(tblClip,strKey.."="..strVal.."\n")		-- V4.0
						end
						local strClip = table.concat(tblClip,"\n")					-- V4.0
						if not general.SaveStringToFile(strClip,strFileName) then
							error("\nSettings file not saved successfully.\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..strFileName.."\n\nError detected.")
						end	
					end
				end
				break
			end
		end
	end -- function SaveSettings

	function fh.CheckWindowPosition(tblName)										-- Ensure dialogue window coordinates are on Screen
		local tblSize = stringx.splitnumbers(tblName.Raster or "600x400","x")	-- Get window dimensions from the previous use of plugin -- V4.4
		local intWinW = tblSize[1]
		local intWinH = tblSize[2]
		if tonumber(tblName.CoordX) == nil
		or tonumber(tblName.CoordY) == nil
		or tonumber(tblName.CoordX) < intMinX										-- V4.4 -- V3.5
		or tonumber(tblName.CoordY) < intMinY										-- V4.4 -- V3.5
		or tonumber(tblName.CoordX) + intWinW > intMaxW							-- V4.4
		or tonumber(tblName.CoordY) + intWinH > intMaxH then						-- V4.4
			tblName.CoordX = iup.CENTERPARENT											-- V4.4
			tblName.CoordY = iup.CENTERPARENT											-- V4.4
		end
	end -- function CheckWindowPosition

	function fh.SetWindowCoord(tblName)												-- Set the Window coordinates if not Maximised or Minimised -- V3.5
		-- tblPosn[1] = origin x, tblPosn[2] = origin y, tblPosn[3] = width, tblPosn[4] = height
		local tblPosn = stringx.splitnumbers(tblName.Dialog.ScreenPosition)
		local intPosX = tblPosn[1] or -1
		local intPosY = tblPosn[2] or -1
		if intPosX < 0 and intPosY < 0 then											-- If origin is negative (-8, -8 = Maximised, -3200, -3200 = Minimised)
			return false																	-- then is Maximised or Minimised
		end
		tblName.CoordX = intPosX														-- Otherwise set the Window coordinates
		tblName.CoordY = intPosY
		return true
	end -- function SetWindowCoord

	function fh.ShowDialogue(strName,iupDialog,btnFocus,strFrame)				-- Set standard frame attributes and display dialogue window
		local tblName = tblNameFor(strName)
		iupDialog = iupDialog or tblName.Dialog										-- Retrieve previous parameters if needed
		btnFocus  = btnFocus  or tblName.Focus
		strFrame  = strFrame  or tblName.Frame
		strFrame  = strFrame  or "show norm"											-- Default frame mode is dialog:showxy(X,Y) with DialogFrame="NO" ("normal" to vary size, otherwise fixed size)
		strFrame  = strFrame:lower()													-- Other modes are "show", "popup" & "keep" with DialogFrame="YES", or with "normal" for DialogFrame="NO" ("show" for active windows, "popup"/"keep" for modal windows)
		if strFrame:gsub("%s-%a-map%a*[%s%p]*","") == "" then						-- May be prefixed with "map" mode to just map dialogue initially, also may be suffixed with "dialog" to inhibit iup.MainLoop() to allow progress messages
			strFrame = "map show norm"													-- If only "map" mode then default to "map show norm"
		end
		if type(iupDialog) == "userdata" then
			tblName.Dialog = iupDialog
			tblName.Focus  = btnFocus													-- Preserve parameters
			tblName.Frame  = strFrame
			iupDialog.Background = fh.Back											-- Background colour
			iupDialog.Shrink = "YES"													-- Sometimes needed to shrink controls to raster size
			if type(btnFocus) == "userdata" then										-- Set button as focus for Esc and Enter keys
				iupDialog.StartFocus   = iupDialog.StartFocus   or btnFocus
				iupDialog.DefaultEsc   = iupDialog.DefaultEsc   or btnFocus
				iupDialog.DefaultEnter = iupDialog.DefaultEnter or btnFocus
			end
			if tblName.CoordX == iup.CENTERPARENT
			or tblName.CoordY == iup.CENTERPARENT then								-- When centred on parent refresh this window size	-- V4.4 -- V4.6
				fh.RefreshDialogue(strName)
			else
				iupDialog.MinSize = "x"													-- Minimum size (default "x" becomes nil)
				iupDialog.RasterSize = tblName.Raster or "x"						-- Raster size  (default "x" becomes nil)
			end
			iupDialog.MaxSize = intMaxW.."x"..intMaxH								-- Maximum size is virtual screen size
			if strFrame:match("norm") then											-- DialogFrame mode is "NO" by default for variable size window
				if strFrame:match("pop") or strFrame:match("keep") then
					iupDialog.MinBox = "NO"												-- For "popup" and "keep" hide Minimize and Maximize icons
					iupDialog.MaxBox = "NO"
				else 
					strFrame = strFrame.." show"										-- If not "popup" nor "keep" then use "showxy" mode
				end
			else
				iupDialog.DialogFrame = "YES"											-- Define DialogFrame mode for fixed size window
			end
			iupDialog.close_cb  = iupDialog.close_cb  or function() return iup.CLOSE end	-- Define default window X close, move, and resize actions
			iupDialog.move_cb   = iupDialog.move_cb   or function(self) if iup.MainLoopLevel() > 0 then fh.SetWindowCoord(tblName) end end			-- V3.5 -- V4.4
			iupDialog.resize_cb = iupDialog.resize_cb or function(self) if iup.MainLoopLevel() > 0 then tblName.Raster=self.RasterSize end end	-- V3.5 -- V4.4 -- V4.6
			fh.RefreshDialogue(strName)												-- Refresh to set Natural Size as Minimum Size
			if strName == "Main" then
				if fhGetAppVersion() > 6 then 										-- Main window centres on FH parent	-- V4.4
					iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
				end
			else
				local tblMain = tblNameFor("Main")									-- Others popup centrally in Main 	-- V4.4
				local iupMain = tblMain.Dialog
				if iupMain then															-- Centre based on size of windows	-- V1.4
					local arrName = stringx.splitnumbers(tblName.Raster or "0x0")
					local arrMain = stringx.splitnumbers(tblMain.Raster or arrName[1].."x"..arrName[2])
					tblName.CoordX = iupMain.X + math.floor( (arrMain[1] - arrName[1]) / 2 )
					tblName.CoordY = iupMain.Y + math.floor( (arrMain[2] - arrName[2]) / 2 )
				elseif fhGetAppVersion() > 6 then 									-- This window centres on FH parent	-- V4.4
					iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
				end
			end
			if strFrame:match("map") then												-- Only dialogue mapping is required
				iupDialog:map()
				tblName.Frame = strFrame:gsub("%s-%a-map%a*[%s%p]*","")			-- Remove "map" from frame mode ready for subsequent call
				return
			end
			if iup.MainLoopLevel() == 0												-- Called from outside Main GUI, so must use showxy() and not popup()
			or strFrame:match("dialog")
			or strFrame:match("sho") then												-- Use showxy() to dispay dialogue window for "showxy" or "dialog" mode
				iupDialog:showxy(tblName.CoordX,tblName.CoordY)
				if not strFrame:match("dialog")										-- Inhibit MainLoop if "dialog" mode	-- V4.1
				and iup.MainLoopLevel() == 0 then iup.MainLoop() end
			else
				iupDialog:popup(tblName.CoordX,tblName.CoordY)						-- Use popup() to display dialogue window for "popup" or "keep" modes
				fhSleep(200,150)															-- Sometimes needed to prevent MainLoop() closure!	-- V3.9
			end
			if not strFrame:match("dialog") and strFrame:match("pop") then
				tblName.Dialog = nil													-- When popup closed, clear key parameters, but not for "keep" mode
				tblName.Raster = nil
				tblName.CoordX = nil -- iup.CENTERPARENT
				tblName.CoordY = nil -- iup.CENTERPARENT
			elseif tblName.CoordX ~= iup.CENTERPARENT								-- V4.4
			   and tblName.CoordY ~= iup.CENTERPARENT then
				fh.SetWindowCoord(tblName)												-- Set Window coordinate pixel values -- V3.5
			end
		end
	end -- function ShowDialogue

	function fh.DestroyDialogue(strName)												-- Destroy existing dialogue
		local tblName = tblNameFor(strName)
		if tblName then
			local iupDialog = tblName.Dialog
			if type(iupDialog) == "userdata" then
				iupDialog:destroy()
				tblName.Dialog = nil													-- Prevent future misuse of handle -- 22 Jul 2014
			end
		end
	end -- function DestroyDialogue

	local function strDialogueArgs(strArgA,strArgB,comp)							-- Compare two argument pairs and return matching pair
		local tblArgA = stringx.splitnumbers(strArgA)
		local tblArgB = stringx.splitnumbers(strArgB)
		local strArgX = tostring(comp(tblArgA[1] or 100,tblArgB[1] or 100))
		local strArgY = tostring(comp(tblArgA[2] or 100,tblArgB[2] or 100))
		return strArgX.."x"..strArgY
	end -- local function strDialogueArgs

	function fh.RefreshDialogue(strName)												-- Refresh dialogue window size after Font change, etc
		local tblName = tblNameFor(strName)
		local iupDialog = tblName.Dialog												-- Retrieve the dialogue handle
		if type(iupDialog) == "userdata" then
			iupDialog.Size = iup.NULL
			iupDialog.MinSize = iup.NULL												-- V3.1
			iup.Refresh(iupDialog)														-- Refresh window to Natural Size and set as Minimum Size
			if not iupDialog.RasterSize then
				iupDialog:map()
				iup.Refresh(iupDialog)
			end
			local strSize = iupDialog.NaturalSize or iupDialog.RasterSize				-- IUP 3.5 NaturalSize = nil, IUP 3.11 needs NaturalSize					-- V3.1
			iupDialog.MinSize = strDialogueArgs(iupDialog.MaxSize,strSize,math.min)	-- Set Minimum Size to smaller of Maximm Size or Natural/Raster Size	-- V3.1
			iupDialog.RasterSize = strDialogueArgs(tblName.Raster,strSize,math.max)	-- Set Current Size to larger of Current Size or Natural/Raster Size	-- V3.1
			iup.Refresh(iupDialog)
			tblName.Raster = iupDialog.RasterSize
			if iupDialog.Visible == "YES" then										-- Ensure visible dialogue origin is on screen
				tblName.CoordX = math.max(tblName.CoordX,10)
				tblName.CoordY = math.max(tblName.CoordY,10)						-- Set both coordinates to larger of current value or 10 pixels
				if iupDialog.Modal then													--	V3.8
					if iupDialog.Modal == "NO" then
						iupDialog.ZOrder = "BOTTOM"									-- Ensure dialogue is subservient to any popup
						iupDialog:showxy(tblName.CoordX,tblName.CoordY)			-- Use showxy() to reposition main window
					else
						iupDialog:popup(tblName.CoordX,tblName.CoordY)				-- Use popup() to reposition modal window
					end
				end
			else
				iupDialog.BringFront="YES"
			end
		end
	end -- function RefreshDialogue

	function fh.AssignAttributes(tblControls)										-- Assign the attributes of all controls supplied
		local anyFunction = nil
		for iupName, tblAttr in pairs ( tblControls or {} ) do
			if type(iupName) == "userdata" and type(tblAttr) == "table" then	-- Loop through each iup control
				local intSkip = 0														-- Skip counter for attributes same for all controls
				for intAttr, anyName in ipairs ( tblControls[1] or {} ) do		-- Loop through each iup attribute
					local strName = nil
					local strAttr = nil
					local strType = type(anyName)
					if strType == "string" then										-- Attribute is different for each control in tblControls
						strName = anyName
						strAttr = tblAttr[intAttr-intSkip]
					elseif strType == "table" then									-- Attribute is same for all controls as per tblControls[1] 
						intSkip = intSkip + 1
						strName = anyName[1]
						strAttr = anyName[2]
					elseif strType == "function" then
						intSkip = intSkip + 1
						anyFunction = anyName
						break
					end
					if type(strName) == "string" and ( type(strAttr) == "string" or type(strAttr) == "function" ) then
						local anyRawGet = rawget(fh,strAttr)							-- Use rawget() to stop require("pl.strict") complaining
						if type(anyRawGet) == "string" then
							strAttr = anyRawGet											-- Use internal module attribute such as Head or FontBody
						elseif type(iupName[strName]) == "string"
						   and type(strAttr) == "function" then						-- Allow string attributes to invoke a function	-- V3.7
							strAttr = strAttr()
						end
						iupName[strName] = strAttr										-- Assign attribute to control
					end
				end
			end
		end
		if anyFunction then anyFunction() end										-- Perform any control assignment function
	end -- function AssignAttributes

	-- Font Dialogue Attributes and Functions --

	fh.FontBody = iup.GetGlobal("DEFAULTFONT")										-- Set default font for Body and Head text
	fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold ")

	function fh.FontDialogue(tblAttr,strName)										-- GUI Font Face & Style Dialogue
		tblAttr = tblAttr or {}
		strName = strName or "Main"
		local isFontChosen = false
		local btnFontHead = iup.button { Title="Choose Headings Font and default Colour"; }
		local btnFontBody = iup.button { Title="Choose Body text Font and default Colour"; }
		local btnCol_Safe = iup.button { Title=" Safe Colour "; }
		local btnCol_Warn = iup.button { Title=" Warning Colour "; }
		local btnCol_Risk = iup.button { Title=" Risky Colour "; }
		local btnDefault  = iup.button { Title=" Default Fonts "; }
		local btnMinimum  = iup.button { Title=" Minimum Size "; }
		local btnDestroy  = iup.button { Title=" Close Dialogue "; }
		local frmSetFonts = iup.frame  { Title=" Set Window Fonts & Colours ";
									iup.vbox { Alignment="ACENTER"; Margin=fh.Margin; Homogeneous="YES";
										btnFontHead;
										btnFontBody;
										iup.hbox { btnCol_Safe; btnCol_Warn; btnCol_Risk; Homogeneous="YES"; };
										iup.hbox { btnDefault ; btnMinimum ; btnDestroy ; Homogeneous="YES"; };
									} -- iup.vbox end
								} -- iup.frame end

		-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
		local dialogFont  = iup.dialog { Title=" Set Window Fonts & Colours "; Gap=fh.Gap; Margin=fh.Border; frmSetFonts; }

		local tblButtons = { }

		local function setDialogues()													-- Refresh the Main dialogue	-- V4.4
			fh.AssignAttributes(tblAttr)												-- Assign parent dialogue attributes
			fh.RefreshDialogue(strName)												-- Refresh parent window size & position and bring infront of other window
			fh.RefreshDialogue("Font")													-- Refresh Font window size & position and bring infront of parent window
		end -- local function setDialogues

		local function getFont(strColor)												-- Set font button function
			local strTitle = " Choose font style & default colour for "..strColor:gsub("Head","Heading").." text "
			local strValue = "Font"..strColor											-- The font codes below are not recognised by iupFontDlg and result in empty font face!
			local strFont  = rawget(fh,strValue):gsub(" Black,",","):gsub(" Light, Bold",","):gsub(" Extra Bold,",","):gsub(" Semibold,",",")
			local iupFontDlg = iup.fontdlg { Title=strTitle; Color=rawget(fh,strColor); Value=strFont; }
			iupFontDlg:popup()															-- Popup predefined font dialogue
			if iupFontDlg.Status == "1" then
				if iupFontDlg.Value:match("^,") then									-- Font face missing so revert to original font
					iupFontDlg.Value = rawget(fh,strValue)
				end
				fh[strColor] = iupFontDlg.Color										-- Set Head or Body color attribute
				fh[strValue] = iupFontDlg.Value										-- Set FontHead or FontBody font style
				fh.AssignAttributes(tblButtons)										-- Assign the button & frame attributes
				setDialogues()
				isFontChosen = true
			end
		end -- local function getFont

		local function getColor(strColor)												-- Set colour button function
			local strTitle = " Choose colour for "..strColor:gsub("Warn","Warning"):gsub("Risk","Risky").." button & message text "
			local iupColorDlg = iup.colordlg { Title=strTitle; Value=rawget(fh,strColor); ShowColorTable="YES"; }
			iupColorDlg.DialogFrame="YES"
			iupColorDlg:popup()															-- Popup predefined color dialogue fixed size window
			if iupColorDlg.Status == "1" then
				fh[strColor] = iupColorDlg.Value										-- Set Safe or Warn or Risk color attribute
				fh.AssignAttributes(tblButtons)										-- Assign the button & frame attributes
				setDialogues()
				isFontChosen = true
			end
		end -- local function getColor

		local function setDefault()													-- Action for Default Fonts button
			fh.Safe = fh.Green
			fh.Warn = fh.Magenta
			fh.Risk = fh.Red																-- Set default colours
			fh.Body = fh.Black
			fh.Head = fh.Black
			fh.FontBody = iup.GetGlobal("DEFAULTFONT")								-- Set default fonts for Body and Head text
			fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold")
			fh.AssignAttributes(tblButtons)											-- Assign the button & frame attributes
			setDialogues()
			isFontChosen = true
		end -- local function setDefault

		local function setMinimum()													-- Action for Minimum Size button
			local tblName = tblNameFor(strName)
			local iupDialog = tblName.Dialog											-- Retrieve the parent dialogue handle
			if type(iupDialog) == "userdata" then
				tblName.Raster = "10x10"												-- Refresh parent window to Minimum Size & adjust position
				fh.RefreshDialogue(strName)
			end
			local tblFont = tblNameFor("Font")
			tblFont.Raster = "10x10"													-- Refresh Font window to Minimum Size & adjust position
			fh.RefreshDialogue("Font")
		end -- local function setMinimum

		tblButtons  =  {  {  "Font" ; "FgColor" ; "Tip" ; "action" ; {"TipBalloon";"Balloon";} ; {"Expand";"YES";} ; };
			[btnFontHead] = { "FontHead"; "Head"; "Choose the Heading text Font Face, Style, Size, Effects, and default Colour"; function() getFont("Head") end; };
			[btnFontBody] = { "FontBody"; "Body"; "Choose the Body text Font Face, Style, Size, Effects, and default Colour"   ; function() getFont("Body") end; };
			[btnCol_Safe] = { "FontBody"; "Safe"; "Choose the colour for Safe operations"   ; function() getColor("Safe") end; };
			[btnCol_Warn] = { "FontBody"; "Warn"; "Choose the colour for Warning operations"; function() getColor("Warn") end; };
			[btnCol_Risk] = { "FontBody"; "Risk"; "Choose the colour for Risky operations"  ; function() getColor("Risk") end; };
			[btnDefault ] = { "FontBody"; "Safe"; "Restore default Fonts and Colours"; function() setDefault() end; };
			[btnMinimum ] = { "FontBody"; "Safe"; "Reduce window to its minimum size"; function() setMinimum() end; };
			[btnDestroy ] = { "FontBody"; "Risk"; "Close this dialogue "; function() return iup.CLOSE end; };
			[frmSetFonts] = { "FontHead"; "Head"; };
			} 

		fh.AssignAttributes(tblButtons)												-- Assign the button & frame attributes
		fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep normal")				-- Popup the Set Window Fonts dialogue: "keep normal"  : vary size & posn, and remember size & posn
	--	fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup normal")			-- Popup the Set Window Fonts dialogue: "popup normal" : vary size & posn, but redisplayed centred
	--	fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep")						-- Popup the Set Window Fonts dialogue: "keep" : fixed size, vary posn, and only remember posn
	--	fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup")						-- Popup the Set Window Fonts dialogue: "popup": fixed size, vary posn, but redisplayed centred
		dialogFont:destroy()
		return isFontChosen

	end -- function FontDialogue

	local function anyMemoControl(anyName,fgColor)									-- Compose any control Title and FgColor
		local strName = tostring(anyName)												-- anyName may be a string, and fgColor is default FgColor
		local tipText = nil
		if type(anyName) == "table" then												-- anyName may be a table = { Title string ; FgColor string ; ToolTip string (optional); }
			strName = anyName[1]
			fgColor = anyName[2]:match("%d* %d* %d*") or fgColor
			tipText = anyName[3]
		end
		return strName, fgColor, tipText
	end -- local function anyMemoControl

	local function anyMemoDialogue(strHead,anyHead,strMemo,anyMemo,...)		-- Display framed memo dialogue with buttons
		local arg = {...}																-- Fix for Lua 5.2+
		local intButt = 0																-- Returned value if "X Close" button is used
		local tblButt = { [0]="X Close"; }											-- Button names lookup table
		local strHead, fgcHead, tipHead = anyMemoControl(anyHead or "",strHead)
		local strMemo, fgcMemo, tipMemo = anyMemoControl(anyMemo or "",strMemo)
		-- Create the GUI labels and buttons
		local lblMemo = iup.label { Title=strMemo; FgColor=fgcMemo; Tip=tipMemo; TipBalloon=fh.Balloon; Alignment="ACENTER"; Padding=fh.Margin; Expand="YES"; WordWrap="YES"; }
		local lblLine = iup.label { Separator="HORIZONTAL"; }
		local iupHbox = iup.hbox  { Homogeneous="YES"; }
		local btnButt = iup.button { }
		local strTop  = "YES"															-- Make dialogue TopMost -- V3.6
		local strMode = "popup"
		if arg[1] == "Keep Dialogue" then												-- Keep dialogue open for a progress message
			strMode = "keep dialogue"
			lblLine = iup.label { }
			if not arg[2] then strTop = "NO" end										-- User chooses TopMost -- V3.6
		else
			if #arg == 0 then arg[1] = "OK" end 										-- If no buttons listed then default to an "OK" button
			for intArg, anyButt in ipairs(arg) do
				local strButt, fgcButt, tipButt = anyMemoControl(anyButt,fh.Safe)
				tblButt[intArg] = strButt
				btnButt = iup.button { Title=strButt; FgColor=fgcButt; Tip=tipButt; TipBalloon=fh.Balloon; Expand="NO"; MinSize="80"; Padding=fh.Margin; action=function() intButt=intArg return iup.CLOSE end; }
				iup.Append( iupHbox, btnButt )
			end
		end
		-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
		local iupMemo = iup.dialog { Title=fh.Plugin..fh.Version..strHead; TopMost=strTop;	-- TopMost added -- V3.6
								iup.vbox { Alignment="ACENTER"; Gap=fh.Gap; Margin=fh.Margin;
									iup.frame { Title=strHead; FgColor=fgcHead; Font=fh.FontHead;
										iup.vbox { Alignment="ACENTER"; Font=fh.FontBody; lblMemo; lblLine; iupHbox; };
									};
								};
							}
		fh.ShowDialogue("Memo",iupMemo,btnButt,strMode)							-- Show popup Memo dialogue window with righthand button in focus (if any)
		if strMode == "keep dialogue" then return lblMemo, iupMemo end			-- Return label & dialogue controls so message can be changed and dialogue destroyed
		iupMemo:destroy()
		return intButt, tblButt[intButt]												-- Return button number & title that was pressed
	end -- local function anyMemoDialogue

	function fh.MemoDialogue(anyMemo,...)											-- Multi-Button GUI like iup.Alarm and fhMessageBox, with "Memo" in frame
		return anyMemoDialogue(fh.Head,"Memo",fh.Body,anyMemo,...)
	end -- function MemoDialogue

	function fh.WarnDialogue(anyHead,anyMemo,...)									-- Multi-Button GUI like iup.Alarm and fhMessageBox, with heading in frame
		return anyMemoDialogue(fh.Warn,anyHead,fh.Warn,anyMemo,...)
	end -- function WarnDialogue

	function fh.GetRegKey(strKey)														-- Read Windows Registry Key Value
		local luaShell = luacom.CreateObject("WScript.Shell")
		local anyValue = nil
		if pcall( function() anyValue = luaShell:RegRead(strKey) end ) then
			return anyValue																-- Return Key Value if found
		end
		return nil
	end -- function GetRegKey

	function fh.PutRegKey(strKey,anyValue,strType)									-- Write Windows Registry Key Value
		local luaShell = luacom.CreateObject("WScript.Shell")
		local strAns = nil
		if pcall( function() strAns = luaShell:RegWrite(strKey,anyValue,strType) end ) then
			return true
		end
		return nil
	end -- function PutRegKey

	local function httpRequest(strRequest)											-- Luacom http request protected by pcall() below
		local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
		http:Open("GET",strRequest,false)
		http:Send()
		return http.Responsebody
	end -- local function httpRequest

	function fh.VersionInStore(strPlugin)											-- Obtain the Version in Plugin Store by Name only	-- V4.5
		local strVersion = "0"
		if strPlugin then
			local strFile = fh.MachinePath.."\\VersionInStore "..strPlugin..".dat"
			general.DeleteFile(strFile)
			local lblMemo, iupMemo = fh.MemoDialogue("Checking for updated version in the Family Historian 'Plugin Store'.","Keep Dialogue")
			local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..strPlugin
			local isOK, strReturn = pcall(httpRequest,strRequest)
			iupMemo:destroy()
			if not isOK then																-- Problem with Internet access
				fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ","MB_OK","MB_ICONEXCLAMATION")
			elseif strReturn ~= nil then
				strVersion = strReturn:match("([%d%.]*),%d*")						-- Version digits & dots then comma and Id digits 
			end
		end
		return strVersion or "0"
	end -- function VersionInStore

	local function intVersion(strVersion)											-- Convert version string to comparable integer
		local intVersion = 0
		local arrNumbers = {}
		strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)	-- V4.1
		for i=1,5 do
			intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
		end
		return intVersion
	end -- local function intVersion

	function fh.CheckVersionInStore()													-- Check if later Version available in Plugin Store	-- V4.5
		local strNewVer = fh.VersionInStore(fh.Plugin:gsub(" %- .*",""))
		local strOldVer = fh.Version
		if intVersion(strNewVer) > intVersion(strOldVer:match("%D*([%d%.]*)")) then
			fh.MemoDialogue("Later Version "..strNewVer.." of this Plugin is available from the 'Plugin Store'.")
		else
			fh.MemoDialogue("No later Version of this Plugin is available from the 'Plugin Store'.")
		end
	end -- function CheckVersionInStore

	function fh.PluginDataScope(strScope)											-- Set default Plugin Data scope to per-Project, or per-User, or per-Machine
		strScope = tostring(strScope):lower()
		if strScope:match("mach") then												-- Per-Machine
			strDefaultScope = "Machine"
		elseif strScope:match("user") then											-- Per-User
			strDefaultScope = "User"
		end																					-- Per-Project is default
	end -- function PluginDataScope

	local function getPluginDataFileName(strScope)									-- Get plugin data filename for chosen scope
		local isOK, strDataFile = pcall(fhGetPluginDataFileName,strScope)
		if not isOK then strDataFile = fhGetPluginDataFileName() end			-- Before V5.0.8 parameter is disallowed and default = CURRENT_PROJECT
		return strDataFile
	end -- local function getPluginDataFileName

	local function getDataFiles(strScope)											-- Compose the Plugin Data file & path & root names
		local strPluginName = fh.Plugin
		local strPluginPlain = stringx.plain(strPluginName)
		local strDataRoot = ""															-- Plugin data file root name -- V4.2
		local strDataPath = ""															-- Plugin data folder path name
		local strDataFile = getPluginDataFileName(strScope)						-- Allow plugins with variant filenames to use same plugin data files
		strDataFile = strDataFile:gsub("\\"..strPluginPlain:gsub(" ","_"):lower(),"\\"..strPluginName)
		strDataFile = strDataFile:gsub("\\"..strPluginPlain..".+%.[D,d][A,a][T,t]$","\\"..strPluginName..".dat")
		if #strDataFile > 0 then														-- Standalone GEDCOM path is ""
			strDataPath = strDataFile:gsub("\\"..strPluginPlain.."%.[D,d][A,a][T,t]$","")
			strDataRoot = strDataPath.."\\"..strPluginName
			general.MakeFolder(strDataPath)											-- V3.4
		end
		return strDataFile, strDataPath, strDataRoot
	end -- local function getDataFiles

	function fh.Initialise(strVersion,strPlugin)									-- Initialise the GUI module with optional Version & Plugin name

		local strAppData = fhGetContextInfo("CI_APP_DATA_FOLDER")
		fh.Plugin = fhGetContextInfo("CI_PLUGIN_NAME")								-- Plugin Name from file
		fh.Version = strVersion or " "												-- Plugin Version
		if fh.Version == " " then
			local strTitle = "\n@Title is missing"
			local strAuthor = "\n@Author is missing"
			local strVersion = "\n@Version is missing"
			local strPlugin = strAppData.."\\Plugins\\"..fh.Plugin..".fh_lua"
			if general.FlgFileExists(strPlugin) then
				for strLine in io.lines(strPlugin) do								-- Read each line from the Plugin file
					strPlugin = strLine:match("^@Title:[\t-\r ]*(.*)")
					if strPlugin then
						strPlugin = strPlugin:gsub("&&","&")
	--?					if fh.Plugin:match("^"..strPlugin:gsub("(%W)","%%%1")) then
						if fh.Plugin:match("^"..stringx.plain(strPlugin)) then
							fh.Plugin = strPlugin										-- Prefer Title to Filename if it matches
							strTitle = nil
						else
							strTitle = "\n@Title differs from Filename"				-- Report abnormality
						end
					end
					if strLine:match("^@Author:%s*(.*)") then						-- Check @Author exists
						strAuthor = nil
					end 
					fh.Version = strLine:gsub("^@Version:%D*([%d%.]*)%D*"," %1 ")
					if fh.Version ~= strLine then										-- Obtain the @Version from Plugin file
						strVersion = nil
						break
					end
				end
				if strTitle or strAuthor or strVersion then							-- Report any header abnormalities
					fhMessageBox("\nScript Header: "..fh.Plugin..(strTitle or "")..(strAuthor or "")..(strVersion or ""),"MB_OK","MB_ICONEXCLAMATION")
				end
			else
				fhMessageBox("\nPlugin has not been saved!","MB_OK","MB_ICONEXCLAMATION")
			end
		end
		fh.History = fh.Version															-- Version History
		fh.Plugin  = strPlugin or fh.Plugin											-- Plugin Name from argument or default from file

		fh.DefaultDialogue()															-- Default "Font","Main" dialogues

		fh.MachineFile,fh.MachinePath,fh.MachineRoot = getDataFiles("LOCAL_MACHINE")		-- Plugin data names per machine
		fh.PerUserFile,fh.PerUserPath,fh.PerUserRoot = getDataFiles("CURRENT_USER")		-- Plugin data names per user
		fh.ProjectFile,fh.ProjectPath,fh.ProjectRoot = getDataFiles("CURRENT_PROJECT")	-- Plugin data names per project

		fh.FhDataPath = fhGetContextInfo("CI_PROJECT_DATA_FOLDER")							-- Paths used by Load/SaveFolder for relative folders		-- V4.0
		fh.PublicPath = fhGetContextInfo("CI_PROJECT_PUBLIC_FOLDER")						-- Public data folder path name		-- V4.0
		if fh.FhDataPath == "" then
			fh.FhDataPath = fh.ProjectPath:gsub("\\Plugin Data$","")
		end
		if fh.PublicPath == "" then
			fh.PublicPath = fh.ProjectPath
			fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Plugin Data$","%1")
		else
			general.MakeFolder(fh.PublicPath)											-- V3.4
			fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Public$","%1")
		end
		fh.CalicoPie = strAppData:gsub("\\Calico Pie\\.*","\\Calico Pie")		-- Program Data Calico Pie path name

		fh.ComputerName = os.getenv("COMPUTERNAME")									-- Local PC Computer Name

	end -- function Initialise

	fh.Initialise()																		-- Initialise module with default values

	return fh

end -- local function iup_gui_v3

local iup_gui = iup_gui_v3()												-- To access FH IUP GUI build module

-- Preset Global Data Definitions --
function PresetGlobalData()

	-- Compose Date & Time Suffix for Log files --
	local function strDateTime()
		local tblDate = {}
		tblDate = os.date("*t")
		return " - "..string.format("%04d",tblDate["year"]).."-"..string.format("%02d",tblDate["month"]).."-"..string.format("%02d",tblDate["day"]).." "..string.format("%02d",tblDate["hour"])..string.format("%02d",tblDate["min"])..string.format("%02d",tblDate["sec"])
	end -- local function strDateTime

	local function strToUTF8(strFileName)
		if string.encoding() == "ANSI" then return strFileName end
		return fhConvertANSItoUTF8(strFileName)
	end -- local function strToUTF8

	-- GUI Global Constants
	iup_gui.Gap = "4"
	iup_gui.Margin = "3x1"
	iup_gui.Balloon = "NO"												 	-- Needed for PlayOnLinux/Mac	-- V3.1	-- Tip Balloon popup 13 Nov 2013
	iup_gui.SetUtf8Mode()

	StrPluginTitle	= iup_gui.Plugin										-- UTF-8 file details	-- V3.7
	StrProjectPath	= iup_gui.ProjectPath
	StrLogFileName	= StrPluginTitle..strDateTime()..".log"
	StrLogFilePath	= StrProjectPath.."\\"..StrLogFileName

	IntMaxNameLen 	= 22													-- Length at which Button label Tag Names are truncated
	ArrLogFileText	= {}
	StrLogNeedTitle	= "Log Edits to File"								-- Radio button titles
	StrPatternTitle	= "Pattern Filters"
	StrConfirmTitle	= "Confirm all Edits"
	StrWarningTitle	= "Show all Warnings"
	StrOldData		= ""													-- Description of Source Tag e.g. Birth, Occupation, etc
	StrNewData		= ""													-- Description of Target Tag

	-- Statistics for Tags processed

	IntSkippedData	=	0													-- Count of current skipped Tags
	IntDeletedData	=	0													-- Count of current deleted Tags
	IntChangedData	=	0													-- Count of current changed Tags
	IntWarningData	=	0													-- Count of current warning Messages
	IntSkippedTotal	=	0													-- Count of total skipped Tags
	IntDeletedTotal	=	0													-- Count of total deleted Tags
	IntChangedTotal	=	0													-- Count of total changed Tags
	IntWarningTotal	=	0													-- Count of total warnings

	-- Global constants

	StrDeleteTag		= "Delete"											-- New Tag value to signal delete Old Tag
	StrSkipTag		= "Skip"												-- New Tag value to signal skip Old Tag
	StrModeOld		= "Old"												-- Parameter value to select only Old Tag
	StrModeNew		= "New"												-- Parameter value to select only New Tag
	StrModeAll		= "All"												-- Parameter value to select both Old & New Tag
	IntHelpCheck		=	1													-- Parameter value to select Warning GUI Check Tags compatible message
	IntHelpNoted		=	2													-- Parameter value to select Warning GUI Value in Source Note message
	IntHelpReport 	=	3													-- Parameter value to select Warning GUI Report to FHUG Forums message
	IntFilterValue	=	1													-- Filter index for Tag selection Value Filter
	IntFilterDate 	=	2													-- Filter index for Tag selection Date Filter
	IntFilterPlace	=	3													-- Filter index for Tag selection Place Filter
	IntFilterRecord	=	4													-- Filter index for Records selection Filter	-- V2.9

	-- Predefined Tag Set Name constants for lookup tables

	DicType			= { E = "  (Events)"; A = "  (Attributes)"; }	-- Divide each Fact Set into Events & Attributes		-- V3.7
	StrStandard		= "Standard"											-- Standard Fact Set name, file & folder
	StrStandEvents	= StrStandard..DicType.E							-- Standrad Events Fcat Set Name							-- V3.7
	StrCustomEvents	= "<Custom Events>"									-- Dummy Undefined Custom Events Set			-- FH V7	-- V3.1
	StrCustomAttrs	= "<Custom Attributes>"								-- Dummy Undefined Custom Attributes Set	-- FH V7	-- V3.1
	StrDefinedTags	= "<Defined Tags>"									-- Dummy Gedcom 5.5 Defined Tags Set
	StrUDFTagsSet 	= "<UDF Tags>"										-- Dummy Uncategorised Data Field (UDF) Tags Set

	TblRecordTag		= {"INDI";"FAM";"NOTE";"SOUR";"REPO";"OBJE";"_PLAC";"SUBM";"SUBN";"_RNOT";"_SRCT";"HEAD";}
	TblRecordName	= {"Individual";"Family";"Note";"Source";"Repository";"Multimedia";"Place";"Submitter";"Submission";"Resource Note";"Source Template";"Header";}

	-- Tag code & name table variables

	TblOldTagIndex	= {}													-- TblOldTagIndex[strRecTag][strTagSet][intItem] = strOldTag and TblOldTagIndex[strRecTag][strTagSet][strOldTag] = true for Source Name dropdown lists
	TblNewTagIndex	= {}													-- TblNewTagIndex[strRecTag][strTagSet][intItem] = strNewTag and TblNewTagIndex[strRecTag][strTagSet][strOldTag] = true for Target Name dropdown lists
	TblSetNameIndex	= {}													-- TblSetNameIndex[strRecTag][intItem] = strSetName for Source/Target Set dropdown lists
	TblDictionary 	= {}													-- TblDictionary[strRecTag][strTag] = strName and TblDictionary[strRecTag][strTagSet][strName] = strTag both way translations

	for intRecTag,strRecTag in ipairs(TblRecordTag) do
		TblOldTagIndex [strRecTag] = {}
		TblNewTagIndex [strRecTag] = {}
		TblSetNameIndex[strRecTag] = {}
		TblDictionary  [strRecTag] = {}
		for intItem, strSetName in ipairs({StrCustomAttrs;StrCustomEvents;StrDefinedTags;StrUDFTagsSet;}) do	-- FH V7	-- V3.1
			TblOldTagIndex[strRecTag][strSetName] = {}
			TblNewTagIndex[strRecTag][strSetName] = {}
			TblDictionary [strRecTag][strSetName] = {}					-- V2.5
		end
		-- Special RecordName v RecordTag both way translations
		TblDictionary[TblRecordName[intRecTag]] = strRecTag
		TblDictionary[strRecTag]["%"] = TblRecordName[intRecTag]
	end

end -- function PresetGlobalConstants

-- Reset Sticky Settings to Default Values --
function ResetDefaultSettings()
	iup_gui.CustomDialogue("Main","0x0")									-- Custom "Main" dialogue minimum size & centralisation
	iup_gui.CustomDialogue("Font","0x0")									-- Custom "Font" dialogue minimum size & centralisation
	iup_gui.CustomDialogue("Memo","0x0")									-- Custom "Memo" dialogue minimum size & centralisation
	iup_gui.CustomDialogue("Logs","0x0")									-- Custom "Logs" dialogue minimum size & centralisation
	iup_gui.DefaultDialogue("Bars","Memo","Logs")						-- GUI window position X & Y co-ordinates, and minsize & rastersize for "Main","Font","Bars","Memo","Logs" dialogues
	StrLogNeed = "ON"														-- Radio button "ON"/"OFF" states
	StrPattern = "ON"
	StrConfirm = "ON"
	StrWarning = "ON"
end -- function ResetDefaultSettings

-- Load Sticky Settings from File --
function LoadSettings()
	iup_gui.LoadSettings("Bars","Memo","Logs")							-- Includes "Main","Font","Bars","Memo","Logs" dialogues and "FontSet" & "History"
	StrLogNeed = tostring(iup_gui.LoadGlobal("LogNeed",StrLogNeed))
	StrPattern = tostring(iup_gui.LoadGlobal("RegExpr",StrPattern))	-- Legacy filter option
	StrPattern = tostring(iup_gui.LoadGlobal("Pattern",StrPattern))
	StrConfirm = tostring(iup_gui.LoadGlobal("Confirm",StrConfirm))
	StrWarning = tostring(iup_gui.LoadGlobal("Warning",StrWarning))
	SaveSettings()															-- Save sticky data settings
end -- function LoadSettings

-- Save Sticky Settings to File --
function SaveSettings()
	iup_gui.SaveGlobal("LogNeed",StrLogNeed)
	iup_gui.SaveGlobal("Pattern",StrPattern)
	iup_gui.SaveGlobal("Confirm",StrConfirm)
	iup_gui.SaveGlobal("Warning",StrWarning)
	iup_gui.SaveSettings("Bars","Memo","Logs")							-- Includes "Main","Font","Bars","Memo","Logs" dialogues and "FontSet" & "History"
end -- function SaveSettings

-- Determine if a Table has Data --
function FlgTableHasData(tblTable)
	if next(tblTable) == nil then
		return false
	else
		return true
	end
end -- function FlgTableHasData

-- Insert a Tag in Old &/or New Tag Index of Tags defined by Tag Set
function DoInsertTagIndex(strRecTag,strSet,strTag,strMode)
	-- strRecTag	~ Record Tag
	-- strSet		~ Tag Set name
	-- strTag		~ Tag name
	-- strMode	~ Insert mode:-
	--				= StrModeOld to only update Old Tag Index
	--				= StrModeNew to only update New Tag Index
	--				= StrModeAll to update both Old & New Index
	if strMode == StrModeOld or strMode == StrModeAll then
		local tblOldTagIndex = TblOldTagIndex[strRecTag][strSet]		-- V2.5
		if not tblOldTagIndex[strTag] then								-- V2.5
			tblOldTagIndex[strTag] = true
			table.insert(tblOldTagIndex,strTag)							-- Add Tag to Old Tag list
		end
	end
	if strMode == StrModeNew or strMode == StrModeAll then
		local tblNewTagIndex = TblNewTagIndex[strRecTag][strSet]		-- V2.5
		if not tblNewTagIndex[strTag] then								-- V2.5
			tblNewTagIndex[strTag] = true
			table.insert(tblNewTagIndex,strTag)							-- Add Tag to New Tag list
		end
	end
end -- function DoInsertTagIndex

-- Remove a Tag from Old Tag Index of Tags defined by Tag Set
function DoRemoveTagIndex(strRecTag,strOldSet,strOldTag)
	-- strRecTag	~ Record Tag
	-- strOldSet	~ Source Set name
	-- strOldTag	~ Source Tag name
	local tblOldTagIndex = TblOldTagIndex[strRecTag][strOldSet]		-- V2.5
	if tblOldTagIndex[strOldTag] then										-- V2.5
		for intTag,strTag in ipairs(tblOldTagIndex) do
			if strTag == strOldTag then
				table.remove(tblOldTagIndex,intTag)
				tblOldTagIndex[strOldTag] = nil
				break
			end
		end
	end
end -- function DoRemoveTagIndex

-- Load Standard & Custom & UDF Facts & Tags --
function LoadFactsAndTags()

	-- Read any Group Index or Fact Set File							-- V3.7
	local function strReadFile(strFile)
		local strText
		if fhGetAppVersion() > 6 then
			strText = fhLoadTextFile(strFile,"UTF-16LE")				-- FH V7 files are UTF-16 format
		else
			local strUtf16 = "^.%z"											-- FH V6 or earlier caters for any format
			local bomUtf16 = "^"..string.char(0xFF,0xFE)				-- "ÿþ"
			local bomUtf8  = "^"..string.char(0xEF,0xBB,0xBF)			-- ""
			local fncConv  = tostring										-- Function to convert input to current encoding
			local strEncoding = string.encoding()
			local fHandle = general.OpenFile(strFile,"rb")
			strText = fHandle:read("*a")									-- Read entire file
			if strText:match(bomUtf16)
			or strText:match(strUtf16) then
				strText = strText:gsub(bomUtf16,"") 				 		-- Strip UTF-16 BOM
				if strEncoding == "ANSI" then								-- Define UTF-16 conversion to current encoding
					fncConv = encoder.StrUTF16_ANSI
				else
					fncConv = encoder.StrUTF16_UTF8
				end
			elseif strText:match(bomUtf8) then
				strText = strText:gsub(bomUtf8,"")						-- Strip UTF-8 BOM
				if strEncoding == "ANSI" then 							-- Define UTF-8 conversion to current encoding
					fncConv = encoder.StrUTF8_ANSI
				end
			else
				if strEncoding == "UTF-8" then							-- Define ANSI conversion to current encoding
					fncConv = encoder.StrANSI_UTF8
				end
			end
			strText = fncConv(strText)										-- Convert the text
		end
		return strText
	end -- local function strReadFile

	-- Load a Facts File into Facts Tables
	local function doLoadFactsTable(dicFactSet)							-- V3.0
		local strFactsFile = dicFactSet.File								-- V3.0
		local strFactsName = dicFactSet.Name								-- V3.0
		local strRecTag, strOldTag, strFact, strType					-- V2.6 -- V3.7
		-- Parse a Fact Item from a Fact File 
		local function strstrParseFactItem(strItem)
			-- Standard Facts have format such as TAG-IE or TAG-FA
			-- Custom Facts have format such as EVEN-TAG-IE or _ATTR-TAG-FA or FACT-TAG-FA			-- FH V7	-- V3.1
			-- Witness Roles have format such as TAG-IE-ROLE or TAG-FA-ROLE to be ignored
			local strTag,strEnd,strType,strRole = strItem:match("^(.+)%-([IF])([EA])([%-ROLE]-)$")	-- V2.7 -- V3,7
			if strRole == "-ROLE" then										-- Ignore Witness Roles 		-- V2.7
				return nil,nil
			elseif strEnd == "I" then										-- "IA" Indiv Attr or "IE" Indiv Event
				return strTag, "INDI", DicType[strType]					-- V3.7
			elseif strEnd == "F" then										-- "FA" Family Attr or "FA" Family Event
				return strTag, "FAM" , DicType[strType]					-- V3.7
			else
				error("\n\n Unrecognised fact item "..strItem.." \n in "..strFactsFile.." \n\n")
			end
		end -- local function strstrParseFactItem

		for _, strType in ipairs ({ "A"; "E"; }) do						-- Create empty Fact Set tables	-- V3.7
			local strFactSet = strFactsName..DicType[strType]
			TblOldTagIndex["INDI"][strFactSet] = {}
			TblNewTagIndex["INDI"][strFactSet] = {}
			TblDictionary ["INDI"][strFactSet] = {}						-- V2.5
			TblOldTagIndex["FAM"] [strFactSet] = {}
			TblNewTagIndex["FAM"] [strFactSet] = {}
			TblDictionary ["FAM"] [strFactSet] = {}						-- V2.5
		end

		for strLine in strReadFile(strFactsFile):gmatch("[^\r\n]+") do						-- V3.7
			local strPref, strSuff = strLine:match("^([%[%a]-)%d-[=%-](.-)%]?$")			-- V2.6
			if strPref and strSuff then
				if strPref:match("^%[") and strRecTag and strOldTag and strFact then		-- V2.8
					DoInsertTagIndex(strRecTag,strFactsName..strType,strOldTag,StrModeNew)-- V3.7
					TblDictionary[strRecTag][strOldTag] = strFact		-- Save the Fact Tag details
					strRecTag = nil
					strOldTag = nil
					strFact = nil
				end
				if strPref == "Item"										-- Found a Fact Tag item such as Item21=EVEN-TAG-IE or Item=TAG-FA
				or strPref == "[FCT" then									-- Found start of Fact definition such as [FCT-TAG-IE]
					strOldTag,strRecTag,strType = strstrParseFactItem(strSuff)
					strFact = nil												-- V2.5 -- V4.1
				elseif strPref == "Name" then								-- Found Fact Name to save against Tag
					strFact = strSuff 										-- V2.5
				elseif strPref == "Label" then							-- Found Fact Label preferred to Fact Name
					if #strFact > IntMaxNameLen
					or #strSuff <= IntMaxNameLen then
						strFact = strSuff									-- V2.5
					end
				elseif strPref == "Abbr" and #strSuff > 0 then			-- Found Fact Abbr to replace long Fact Name -- V3.0
					if  #strFact > IntMaxNameLen
					and #strSuff <= IntMaxNameLen then
						strFact = strSuff									-- V2.6
					end
				elseif strPref == "Hidden" and strSuff == "Y" then
					strFact = strFact.." <hidden>"						-- Fact Name is <hidden>	-- V2.8
					strOldTag = strOldTag.."<h>"							-- Fix for <hidden> issue	-- V4.1
				end
			end
		end
		for _, strRecTag in ipairs({ "INDI"; "FAM"; }) do
			local tblSetNameIndex = TblSetNameIndex[strRecTag]
			for _, strType in ipairs({ "A"; "E"; }) do					-- Save the Fact Set tables	-- V3.7
				local strFactSet = strFactsName..DicType[strType]
				if strFactsName:match(" %(project%)$") then
					table.insert(tblSetNameIndex,1,strFactSet)			-- Ensure that a (project) fact set goes above Standard	-- V3.5
				elseif strFactSet ~= StrStandard..DicType[strType] then
					table.insert(tblSetNameIndex,3,strFactSet)			-- Reverse order of Fact Sets, but with Standard at top	-- V2.6
				end
				for intOldTag,strOldTag in ipairs(TblNewTagIndex[strRecTag][strFactSet]) do
					local strName = TblDictionary[strRecTag][strOldTag]
					TblDictionary[strRecTag][strFactSet][strName] = strOldTag	-- V2.5
				end
			end
		end

	end -- local function doLoadFactsTable

	-- Load Gedcom Defined Tags to Dummy Tag Set
	local function doLoadDefinedTags(strRecTag,tblRecTag)
		for strOldTag,strOldName in pairs(tblRecTag) do
			DoInsertTagIndex(strRecTag,StrDefinedTags,strOldTag,StrModeNew)
			TblDictionary[strRecTag][strOldTag] = strOldName
			TblDictionary[strRecTag][StrDefinedTags][strOldName] = strOldTag	-- V2.5
		end
	end -- local function doLoadDefinedTags

	-- Load Undefined Tags to Dummy Tag Sets
	local function doLoadUndefinedTags()
		local strSuffix = ""
		local flgTagFound = false
		local strOldTag = ""
		local ptrOldTag = fhNewItemPtr()
		local ptrRecord = fhNewItemPtr()
		for intRecTag,strRecTag in ipairs(TblRecordTag) do
			ptrRecord:MoveToFirstRecord(strRecTag)
			while not ptrRecord:IsNull() do														-- Search each Record
				ptrOldTag:MoveToFirstChildItem(ptrRecord)
				while not ptrOldTag:IsNull() do													-- Search each Tag
					strOldTag = fhGetTag(ptrOldTag)
					if fhIsEvent(ptrOldTag) or fhIsAttribute(ptrOldTag) then					-- Found an Event/Attribute Fact
						for intSet,strSet in ipairs(TblSetNameIndex[strRecTag]) do			-- Search existing Fact Sets and Fact Tags
							flgTagFound = false
							if TblNewTagIndex[strRecTag][strSet][strOldTag] then				-- V2.5
								DoInsertTagIndex(strRecTag,strSet,strOldTag,StrModeOld)		-- Add current Fact Tag to Old Tag index for Fact Set
								flgTagFound = true
								break																	-- Escape if Fact Tag is found
							end
						end
						if not flgTagFound then														-- Add undefined Tag to dummy undefined Custom Events/Attributes fact set	-- V3.1
							local strOldDisp = fhGetDisplayText(ptrOldTag)..("  "):rep(9)	-- Extract undefined fact name	-- V3.1 -- V3.9
							local _, intLast = strOldDisp:find("^(.-) ",math.max(1,#strOldTag-5))	-- V4.2 -- Ensure index is never negative
							local strOldName = strOldDisp:sub(1,intLast-1):gsub(" +$","")	-- V3.9
							local strCustom  = StrCustomEvents
							if fhIsAttribute(ptrOldTag) then strCustom = StrCustomAttrs end
							DoInsertTagIndex(strRecTag,strCustom,strOldTag,StrModeAll)
							TblDictionary[strRecTag][strOldTag] = strOldName
							TblDictionary[strRecTag][strCustom][strOldName] = strOldTag		-- V2.5
						end
					elseif fhIsUDF(ptrOldTag) then												-- Found UDF Tag
						strSuffix = " UDF Tag"
						if strRecTag == "INDI" or strRecTag == "FAM" then
							local strOldVal = fhGetValueAsText(ptrOldTag)
							local ptrLink,strTag = PtrStrTranslateLink(strOldVal)				-- If value is a link such as @S99@ then not valid as a Fact
							if ptrLink == nil then
								strSuffix = " UDF Attribute"
								if strOldVal == "" and TblNewTagIndex[strRecTag][StrUDFTagsSet][strOldTag] ~= strOldTag..strSuffix then
									strSuffix = " UDF Event"										-- If its value is null, and not already an Attribute, then it is an Event
								end
							end
						end
						DoInsertTagIndex(strRecTag,StrUDFTagsSet,strOldTag,StrModeAll)		-- Add uncategorised Data Tag to dummy UDF Tag Set
						TblDictionary[strRecTag][strOldTag] = strOldTag..strSuffix
						TblDictionary[strRecTag][StrUDFTagsSet][strOldTag..strSuffix] = strOldTag	-- V2.5
					else
						DoInsertTagIndex(strRecTag,StrDefinedTags,strOldTag,StrModeOld)		-- Add current defined Data Tag to Old Tag index
						if TblDictionary[strRecTag][strOldTag] == nil then
							TblDictionary[strRecTag][strOldTag] = strOldTag.." not Defined !"	-- Fallback in case Tag is not in <Defined Tags> Set
							TblDictionary[strRecTag][StrDefinedTags][strOldTag.." not Defined !"] = strOldTag	-- V2.5
						end
					end
					ptrOldTag:MoveNext("ANY")
				end
				ptrRecord:MoveNext("SAME_TAG")													-- Move to next Record
			end
			for intItem,strSetName in ipairs({StrCustomEvents;StrCustomAttrs;StrDefinedTags;StrUDFTagsSet;}) do
				if FlgTableHasData(TblOldTagIndex[strRecTag][strSetName]) then
					table.insert(TblSetNameIndex[strRecTag], strSetName)
				end
			end
		end
	end -- local function doLoadUndefinedTags()

	for _, strRecTag in ipairs({ "INDI"; "FAM"; }) do											-- Ensure Standard is always top of list except for local (project) fact set	-- V3.7
		for _, strType in ipairs({ "A"; "E"; }) do
			local strFactSet = StrStandard..DicType[strType]
			table.insert(TblSetNameIndex[strRecTag], 1, strFactSet)
		end
	end

	-- Use the GroupIndex to process Fact Sets in reverse order to obscure <eclipsed> Facts (like Export Gedcom File)	-- V2.6 -- V3.0
	local dicGroupIndex = {}
	local strDataFolder = fhGetContextInfo("CI_PROJECT_DATA_FOLDER").."\\Fact Types"
	local strGroupIndex = strDataFolder.."\\GroupIndex.fhdata"								-- Only applies to FH V7 or later
	if general.FlgFileExists(strGroupIndex) then												-- Read each line in Project only GroupIndex	-- V3.2 -- V3.4 -- V3.7
		for strFactSet, intFactSet in general.StrLoadFromFile(strGroupIndex):gmatch("([^\r\n]+)=(%d+)") do
			intFactSet = tonumber(intFactSet)														-- Extract Fact Set name and ordinal position
			if strFactSet and intFactSet
			and not dicGroupIndex[intFactSet] then												-- Compose Fact Set filename
				local strFactsFile = strDataFolder.."\\"..strFactSet..".fhf"
				if general.FlgFileExists(strFactsFile) then										-- Fact Set name and file exist, so save in ordinal order
					dicGroupIndex[intFactSet] = { Name=strFactSet.." (project)"; File=strFactsFile; }
				end
			end
		end
	end
	local intGroupIndex = #dicGroupIndex
	local strDataFolder = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Fact Types"
	local strGroupIndex = strDataFolder.."\\Standard\\GroupIndex.fhdata"
	if general.FlgFileExists(strGroupIndex) then												-- Read each line in ProgramData GroupIndex	-- V3.0 -- V3.7
		for strFactSet, intFactSet in strReadFile(strGroupIndex):gmatch("([^\r\n]+)=(%d+)") do
			intFactSet = tonumber(intFactSet)
			if strFactSet and intFactSet
			and not dicGroupIndex[intFactSet+intGroupIndex] then								-- Compose Fact Set filename -- V3.0
				local strFactsFile = strDataFolder.."\\Custom\\"..strFactSet..".fhf"
				if strFactSet == StrStandard then
					strFactsFile = strDataFolder.."\\Standard\\Standard.fhf"
				end
				if general.FlgFileExists(strFactsFile) then										-- Fact Set name and file exist, so save in ordinal order -- V3.0
					dicGroupIndex[intFactSet+intGroupIndex] = { Name=strFactSet; File=strFactsFile; }
				end
			end
		end
	end
	for intFactSet = #dicGroupIndex, 1, -1 do													-- Reverse order of Fact Sets
		doLoadFactsTable(dicGroupIndex[intFactSet])												-- Import Fact Set file to Fact tables -- V3.0
	end

	if not next(TblDictionary.INDI[TblSetNameIndex.INDI[1]]) then table.remove(TblSetNameIndex.INDI,1) end -- V3.7
	if not next(TblDictionary.FAM [TblSetNameIndex.FAM [1]]) then table.remove(TblSetNameIndex.FAM ,1) end -- V3.7

	-- Load common GEDCOM 5.5 1996 & Place Defined Tag Sets
	doLoadDefinedTags("INDI" ,{RIN="Automated Record Id";ALIA="Alias";ANCI="Ancestor Interest";ASSO="Associated Person";REFN="Custom Id";DESI="Descendant Interest";_FLGS="Flags";CHAN="Last Change Notes";AFN="LDS Ancestral File No.";BAPL="LDS Baptism";SLGC="LDS Child Sealing";CONL="LDS Confirmation";ENDL="LDS Endowment";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";NAME="Name";NOTE="Note Record";NOTE2="Note";RFN="Permanent Record No.";FAMC="Parents Family";FAMS="Spouse Family";RESN="Restriction Notice";SEX="Sex";SOUR="Source (citation)";SOUR2="Source Note";SUBM="Submitter";})
	doLoadDefinedTags("FAM"  ,{RIN="Automated Record Id";CHIL="Child";NCHI="Child Count";REFN="Custom Id";HUSB="Husband";CHAN="Last Change Notes";SLGS="LDS Spouse Sealing";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";_STAT="Marriage Status";NOTE="Note Record";NOTE2="Note";SOUR="Source (citation)";SOUR2="Source Note";SUBM="Submitter";WIFE="Wife";})
	doLoadDefinedTags("NOTE" ,{RIN="Automated Record Id";TEXT="Text";REFN="Custom Id";CHAN="Last Change Notes";SOUR="Source (citation)";SOUR2="Source Note";})
	doLoadDefinedTags("SOUR" ,{RIN="Automated Record Id";AUTH="Author";REFN="Custom Id";DATA="Data";CHAN="Last Change Notes";NOTE="Note Record";NOTE2="Note";PUBL="Publication Info";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";REPO="Repository";ABBR="Short Title";TEXT="Text From Source";TITL="Title";_TYPE="Type";})
	doLoadDefinedTags("REPO" ,{RIN="Automated Record Id";ADDR="Address";REFN="Custom Id";CHAN="Last Change Notes";NAME="Name";NOTE="Note Record";NOTE2="Note";PHON="Phone Number";})
	doLoadDefinedTags("OBJE" ,{RIN="Automated Record Id";REFN="Custom Id";_DATE="Date";_KEYS="Keywords";CHAN="Last Change Notes";NOTE="Note Record";})
	doLoadDefinedTags("_PLAC",{CHAN="Last Change Notes";LATLONG="Lat./Longitude";OBJE="Multimedia Object";NOTE2="Note";TEXT="Place Name";STAN="Standardized";STAT="Status";})
	doLoadDefinedTags("SUBM" ,{RIN="Automated Record Id";ADDR="Address";NAME="Name";LANG="Language";CHAN="Last Change Notes";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";PHON="Phone Number";RFN="Permanent Record No.";})
	doLoadDefinedTags("SUBN" ,{RIN="Automated Record Id";ANCE="Ancestor Generations";DESC="Descendant Generations";FAMF="Family File Name";CHAN="Last Change Notes";ORDI="Ordinance Process";SUBM="Submitter";TEMP="Temple Code";})
	doLoadDefinedTags("HEAD" ,{CHAR="Character Set";COPR="Copyright";DATE="Transmission Date";DEST="Receiving System";FILE="Filename";_ROOT="File Root";GEDC="GEDCOM";LANG="Language";NOTE2="File Description";PLAC="Place Format";_ROOT="File Root";_USED="Last-used Record Id";SOUR2="System Id";SUBM="Submitter";SUBN="Submission";_UID="User Id";})
	if fhGetAppVersion() < 7 then
		-- Load variants for GEDCOM 5.5 1996 Defined Tag Sets									-- FH V7	-- V3.1
		doLoadDefinedTags("REPO" ,{_EMAIL="Email";_WEB="Web Site";})
		doLoadDefinedTags("OBJE" ,{BLOB="Bulk Object";_FILE="File";FORM="Format";_NOTE="Note";NOTE2="Link/Note";TITL="Title";})		-- OBJE="Multimedia Link" associated only with BLOB continuation
		doLoadDefinedTags("SUBM" ,{_EMAIL="Email";_WEB="Web Site";})
	else
		-- Load variants for GEDCOM 5.5.1 2019 & Research Note & Source Template Defined Tag Sets	-- FH V7	-- V3.1
		doLoadDefinedTags("INDI" ,{_RNOT="Research Note";_FSID="FamilySearch Id";_UID="Unique Id";})			-- V3.6
		doLoadDefinedTags("FAM"  ,{_RNOT="Research Note";})
		doLoadDefinedTags("SOUR" ,{_RNOT="Research Note";_SRCT="Source Template";_FIELD="Metafield"})			-- V3.3
		doLoadDefinedTags("REPO" ,{_RNOT="Research Note";EMAIL="Email";WWW="Web Site";})
		doLoadDefinedTags("_PLAC",{_RNOT="Research Note";})
		doLoadDefinedTags("OBJE" ,{_RNOT="Research Note";_NOTA="Annotation";FILE="File";NOTE2="Note";})
		doLoadDefinedTags("SUBM" ,{EMAIL="Email";WWW="Web Site";})
		doLoadDefinedTags("HEAD" ,{_PCIT="Prepared Citation";})														-- V3.3
		doLoadDefinedTags("_RNOT",{REFN="Custom Id";CHAN="Last Change Notes";TEXT="Text";})
		doLoadDefinedTags("_SRCT",{BIBL="Bibliography";CATG="Category";COLL="Collection";DESC="Description";FDEF="Field Definition";FOOT="Footnote";CHAN="Last Change Notes";NAME="Name";NOTE2="Note";TITL="Record Title";REFN="Reference";SHRT="Short Footnote";SUBC="Subcategory";})
	end
	-- Load any undefined Custom Fact or Uncategorised Data Field (UDF) into dummy Tag Sets
	doLoadUndefinedTags()

end -- function LoadFactsAndTags

-- Write Line to Log --																				-- V3.7
function WriteLog(strLine)
	if StrLogNeed == "ON" then																		-- Logging enabled
		local strBOM = ""
		if string.encoding() == "UTF-8" then
			strBOM = string.char(0xEF,0xBB,0xBF)													-- UTF-8 BOM
		end
		if LblLogFile.Title ~= StrLogFilePath then												-- Log has just been created
			LblLogFile.Title = StrLogFilePath														-- Report log file path in Main GUI
			ArrLogFileText = {}
			table.insert(ArrLogFileText,strBOM.."Folder\t"..StrProjectPath)					-- Insert folder & name details into log 
			table.insert(ArrLogFileText,"Logfile\t"..StrLogFileName)
			table.insert(ArrLogFileText," ")
		end
		table.insert(ArrLogFileText,strLine)														-- Write data line to log
	end
end -- function WriteLog

-- Update Count Status --
function UpdateCountStatus()

	local function strCountFormat(intCount,strFormat)
		if strFormat == "Warnings." and intCount == 1 then strFormat = "Warning.  " end
		strFormat = string.format("%4d ",intCount)..strFormat
		return strFormat
	end -- local function strCountFormat

	local strCounts = "" 		-- Counts log report
	local strTotals = "" 		-- Totals log report
	local strFormat = ""		-- Formatted data
	-- Report Data Counts
	strFormat = strCountFormat(IntSkippedData,"Skipped.")		LblCoSkipped.Title = strFormat		strCounts = strCounts..strFormat.."\t"
	strFormat = strCountFormat(IntDeletedData,"Deleted.")		LblCoDeleted.Title = strFormat		strCounts = strCounts..strFormat.."\t"
	strFormat = strCountFormat(IntChangedData,"Changed.")		LblCoChanged.Title = strFormat		strCounts = strCounts..strFormat.."\t"
	strFormat = strCountFormat(IntWarningData,"Warnings.")	LblCoWarning.Title = strFormat		strCounts = strCounts..strFormat
	-- Report Total Counts
	strFormat = strCountFormat(IntSkippedTotal,"Skipped.")	LblToSkipped.Title = strFormat		strTotals = strTotals..strFormat.."\t"
	strFormat = strCountFormat(IntDeletedTotal,"Deleted.")	LblToDeleted.Title = strFormat		strTotals = strTotals..strFormat.."\t"
	strFormat = strCountFormat(IntChangedTotal,"Changed.")	LblToChanged.Title = strFormat		strTotals = strTotals..strFormat.."\t"
	strFormat = strCountFormat(IntWarningTotal,"Warnings.")	LblToWarning.Title = strFormat		strTotals = strTotals..strFormat
	WriteLog("Counts\t"..strCounts)
--?	WriteLog("Totals\t"..strTotals)
	WriteLog(" ")
	return strCounts:gsub("\t","  ").."\r\n"
end -- function UpdateCountStatus

-- Get Display of Record Name --
function StrDisplayName(ptrName)
	return fhGetDisplayText(ptrName).." ["..fhGetRecordId(ptrName).."]"
end -- function StrDisplayName

-- Get Display of Record Data --
function StrDisplayData(ptrData)
	return general.BuildDataRef(ptrData)..": "..fhGetDisplayText(ptrData)					 -- V2.5
end -- function StrDisplayData

-- Global tables for Query Result Set Columns
TblPtrName = {}
TblOldItem = {}
TblOldData = {}
TblPerform = {}
TblNewItem = {}
TblPtrData = {}

-- Write Tag Change to Log and Increment Counts --
function WriteDataChange(ptrName,ptrOld,ptrNew,strNew)
	-- ptrName	~ Record pointer
	-- ptrOld		~ Source Tag pointer
	-- ptrNew		~ Target Tag pointer or StrDeleteTag
	-- strNew		~ "" or StrSkipTag or StrDeleteTag
	WriteLog("Name\t'"..TblDictionary[fhGetTag(ptrName)]["%"]..": "..StrDisplayName(ptrName).."'")
	local strLine = "Data\t'"..StrDisplayData(ptrOld)
	table.insert(TblPtrName,ptrName:Clone())
	table.insert(TblOldItem,StrOldData)
	table.insert(TblOldData,StrDisplayData(ptrOld))
	table.insert(TblPerform,"unknown")
	table.insert(TblNewItem,"")
	table.insert(TblPtrData,"")
	if strNew == StrDeleteTag then
		IntDeletedData = IntDeletedData + 1
		IntDeletedTotal = IntDeletedTotal + 1
		strLine = strLine.."'  deleted."
		TblPerform[#TblPtrName] = "deleted"
	elseif strNew == StrSkipTag then
		IntSkippedData = IntSkippedData + 1
		IntSkippedTotal = IntSkippedTotal + 1
		strLine = strLine.."'  skipped."
		TblPerform[#TblPtrName] = "skipped"
		TblPtrData[#TblPtrName] = ptrOld:Clone()
	else
		IntChangedData = IntChangedData + 1
		IntChangedTotal = IntChangedTotal + 1
		strLine = strLine.."'  became  '"..StrDisplayData(ptrNew).."'"
		TblPerform[#TblPtrName] = "changed"
		TblNewItem[#TblPtrName] = StrNewData
		TblPtrData[#TblPtrName] = ptrNew:Clone()
	end
	WriteLog(strLine)
end -- function WriteDataChange

-- Translate NOTE/OBJE/SOUR link @@N99@@ or @O99@ or @@S99@@ into a Pointer and Tag --
function PtrStrTranslateLink(strLink)
	local ptrLink = fhNewItemPtr()																-- return Null pointer if no link found
	local strTag  = nil
	if strLink:sub(1,1) == "@" and strLink:sub(-1) == "@" then
		strLink = strLink:gsub("@","")															-- Starts & ends with @ so looks like a link that needs every @ removed
		local tblTag = { N="NOTE"; O="OBJE"; S="SOUR"; }
		strTag = tblTag[strLink:sub(1,1)]															-- Check and convert initial letter N or O or S to Tag
		if strTag then																				-- V2.5
			strLink = strLink:sub(2)																-- Remove initial letter to leave number ID
			ptrLink = fhNewItemPtr()
			ptrLink:MoveToRecordById(strTag,tonumber(strLink))								-- Obtain link to record using number ID
		end
	end
	return ptrLink,strTag
end -- function PtrStrTranslateLink

-- Edit the Data Details for Source Tag and Target Tag --
function DoEditDataDetails(ptrRecord,ptrOld,strNew)
	-- ptrRecord	~ Record pointer
	-- ptrOld		~ Source Tag pointer
	-- strNew		~ Target Tag or StrDeleteTag

	local ptrNew = fhNewItemPtr()
	local isCont = true							-- Continue changes after Warning	-- V3.8
	local tblLog = {}							-- Log of warnings for SOURce Notes
	local intLog = 0								-- Log count for warnings
	local tblTag = {}							-- Tag data ref hierarchy
	local intTag = 1								-- Tag level for hierarchy
	tblTag[intTag] = fhGetTag(ptrRecord)		-- "INDI","FAM","NOTE","SOUR",etc

	-- Compose Current Data Reference --
	local function strDataReference(strTag)
		local strDataRef = "%"
		for intItem, strName in ipairs(tblTag) do
			strDataRef = strDataRef..strName.."."
		end
		strDataRef = strDataRef..strTag.."%"
		return strDataRef
	end -- local function strDataReference

	-- Log and Report Warning Message --
	local function doReportWarning(strTag,strVal,strWarn,intHelp)
		-- Compose new Data Reference & Value & Warning message
		strWarn = strDataReference(strTag).." "..strVal.." ~ "..strWarn
		intLog = intLog + 1
		tblLog[intLog] = strWarn
		IntWarningData = IntWarningData + 1
		IntWarningTotal = IntWarningTotal + 1
		if StrWarning == "ON" then
			isCont = GUI_WarnDialogue(StrDisplayName(ptrRecord),StrDisplayData(ptrOld),strWarn,intHelp)	-- V3.8
		end
	end -- local function doReportWarning

	-- Copy All Children Items --
	local function doCopyChildrenItems(ptrOld,ptrNew)

		-- Copy All Child Branch Items --
		local function doCopyChildBranch(ptrOrigin,ptrTarget)
			local strTag = fhGetTag(ptrOrigin)
			local strVal = fhGetValueAsText(ptrOrigin)
		--?	local lnkVal = fhGetValueAsLink(ptrOrigin)					-- Diagnostic
		--?	local iValue = fhGetValueAsInteger(ptrOrigin)				-- Diagnostic
		--?	local dtDate = fhGetValueAsDate(ptrOrigin)					-- Diagnostic
		--?	local strTxt = fhGetItemText(ptrOrigin,strTag)				-- Diagnostic
		--?	local strType = fhGetValueType(ptrOrigin)					-- Diagnostic
		--?	local strClas = fhGetDataClass(ptrOrigin)					-- Diagnostic
			local ptrNew = fhNewItemPtr()

			local function doConvertUDF_Link()
				local ptrLink,strLink = PtrStrTranslateLink(strVal)							-- Convert UDF SOUR/OBJE/NOTE Link to Pointer & Tag
				if ptrLink:IsNotNull() then
					ptrNew = fhCreateItem(strLink,ptrTarget)
					if ptrNew:IsNull() then
						doReportWarning(strLink,strVal,"Invalid Tag",IntHelpCheck)
					else
						if not fhSetValueAsLink(ptrNew,ptrLink) then							-- Set Link Pointer as new Tag value
							doReportWarning(strLink,strVal,"Link Pointer value not accepted",IntHelpCheck)
						end
						doCopyChildrenItems(ptrOrigin,ptrNew)
					end
					return nil
				end
				strTag = strTag.."2"																-- Convert UDF SOUR/OBJE/NOTE to text Tag  
				return strTag
			end -- local function doConvertUDF_Link

			local function doConvertUDF_Date()
				ptrNew = fhCreateItem(strTag,ptrTarget)
				if ptrNew:IsNull() then
					doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck)
				else
					local dtDate = fhNewDate(1999)												-- Convert Date string to Date Object
					if not dtDate:SetValueAsText(strVal,false) then
						doReportWarning(strTag,strVal,"Unrecognised 'Date' value, treated as 'Date Phrase'",IntHelpNoted)
						if not dtDate:SetValueAsText(strVal,true) then
							doReportWarning(strTag,strVal,"Unrecognised 'Date Phrase' value",IntHelpNoted)
						else
							if not fhSetValueAsDate(ptrNew,dtDate) then	-- Set Date Object as new Tag value
								doReportWarning(strTag,strVal,"Date Object value not accepted",IntHelpCheck)
							end
						end
					else
						if not fhSetValueAsDate(ptrNew,dtDate) then		-- Set Date Object as new Tag value
							doReportWarning(strTag,strVal,"Date Object value not accepted",IntHelpCheck)
						end
					end
					doCopyChildrenItems(ptrOrigin,ptrNew)
				end
				return nil
			end -- local function doConvertUDF_Date

			local function doConvertUDF_Even()
				ptrNew = fhCreateItem(strTag,ptrTarget)
				if ptrNew:IsNull() then
					doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck)
				else
					local tblEvent = {}																-- EVENt can be EVEN, ADOP, BIRT, BAPM, CAST, etc, etc
					local strEvent = nil
					tblEvent[1] = { EVEN="Event"; CAST="Social Rank"; CHRA="Adult Christening"; }	-- Exceptions not translated below
					tblEvent[2] = TblDictionary["INDI"]											-- All the Individual Fact tag translations
					tblEvent[3] = TblDictionary["FAM"]											-- All the Family Fact tag translations
					for i=1,3 do
						strEvent = tblEvent[i][strVal]
						if strEvent then break end													-- V2.5
					end
					if strEvent == nil then
						doReportWarning(strTag,strVal,"Unrecognised 'Event Type' value",IntHelpReport)
					else
						if not fhSetValueAsText(ptrNew,strEvent) then
							doReportWarning(strTag,strEvent,"Event Type text not accepted",IntHelpCheck)
						end
					end
					doCopyChildrenItems(ptrOrigin,ptrNew)
				end
				return nil
			end -- local function doConvertUDF_Even

			local function doConvertUDF_Quay()
				ptrNew = fhCreateItem(strTag,ptrTarget)
				if ptrNew:IsNull() then
					doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck)
				else																					-- QUAY Certainty can be "0" ; "1" ; "2" ; "3" ;
					local tblCertainty = {"Unreliable";"Questionable";"Secondary evidence";"Primary evidence";}
					local strCertainty = tblCertainty[tonumber(strVal)+1]
					if strCertainty == nil then
						doReportWarning(strTag,strVal,"Unrecognised 'Certainty' value",IntHelpReport)
					else
						if not fhSetValueAsText(ptrNew,strCertainty) then
							doReportWarning(strTag,strCertainty,"Certainty text not accepted",IntHelpCheck)
						end
					end
					doCopyChildrenItems(ptrOrigin,ptrNew)
				end
				return nil
			end -- local function doConvertUDF_Quay

			local function doConvertUDF_Conc_Cont()												-- CONC & CONT are always invalid
				if intTag == 2 and ( fhIsEvent(ptrTarget) or fhGetTag(ptrTarget) == "RESI" ) then
					strTag = "SOUR2"																	-- If at target level 2 of Event or RESI then
					return strTag																		-- Create SOUR2 Note as if Attribute value
				end
				local strValue = "\n"																-- CONTinuation on a new line
				if strTag == "CONC" then
					strValue = ""																		-- CONCatenation on same line
				end
				local strValue = fhGetValueAsText(ptrTarget)..strValue..strVal
				if not fhSetValueAsText(ptrTarget,strValue) then								-- Append to existing Target value
					doReportWarning(strTag,strValue,"Value text not accepted",IntHelpCheck)
				end
				if fhGetValueAsText(ptrTarget) ~= strValue then								-- "\n" does not work for Attribute value ( nor ADDR tag !!!! )
					strValue = fhGetValueAsText(ptrTarget).."~"..strVal
					if not fhSetValueAsText(ptrTarget,strValue) then
						doReportWarning(strTag,strValue,"Value text not accepted",IntHelpCheck)
					end
				end
				return nil
			end -- local function doConvertUDF_Conc_Cont

			if fhIsUDF(ptrOrigin) then
				if strTag == "SOUR" or strTag == "OBJE" or strTag == "NOTE" then
					-- Convert UDF SOUR/OBJE/NOTE link or text
					if doConvertUDF_Link() == nil then
						return
					end
				elseif strTag == "DATE" or strTag == "_DATE" then
					-- Convert UDF DATE/_DATE string to DATE object
					if doConvertUDF_Date() == nil then
						return
					end
				elseif strTag == "EVEN" then
					-- Convert UDF EVEN string
					if doConvertUDF_Even() == nil then
						return
					end
				elseif strTag == "QUAY" then
					-- Convert UDF QUAY string
					if doConvertUDF_Quay() == nil then
						return
					end
				elseif strTag == "CONC" or strTag == "CONT" then									
					-- Convert UDF CONC or CONT tag
					if doConvertUDF_Conc_Cont() == nil then
						return
					end
				end
			end

			if strTag == "_FMT" then return end 													-- Skip rich text format code	-- V3.3

			-- No existing Child Tag so create new Child Tag
			if strTag == "TYPE" then																-- Transfer any TYPE Descriptor to a SOURce Note
				ptrNew = fhCreateItem("SOUR2",ptrTarget)										-- Because it cannot transfer to Custom Facts
				if ptrNew:IsNull() then
					doReportWarning("SOUR2",strVal,"Invalid Tag",IntHelpCheck)
				else
					if not fhSetValueAsText(ptrNew,"TYPE Descriptor: "..strVal) then
						doReportWarning("SOUR2","TYPE Descriptor: "..strVal,"Value text not accepted",IntHelpCheck)
					end
				end
			else
				if strTag == "_FIELD" then 														-- Substitute metafield shortcut	-- V3.3
					strTag = fhGetMetafieldShortcut(ptrOrigin)
				end
				ptrNew = fhCreateItem(strTag,ptrTarget,true)									-- V5.0.2 lets existing empty items to be used, e.g. FILE or FORM auto-created by local OBJEct parent,
				if ptrNew:IsNull() then																-- but FILE may be _FILE within OBJE within UDF, so try Tag without leading underscore.
					ptrNew = fhCreateItem(strTag:gsub("^_",""),ptrTarget,true)
				end
				if ptrNew:IsNull() then
					doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck)
				else
					-- DATA & OBJE2 tags within UDF have null string text value that causes fhSetValue_Copy to fail 
					if not ( fhGetValueType(ptrOrigin) == "text" and strVal == "" ) then
						local strValue = fhGetValueAsText(ptrOrigin)
						if not fhSetValue_Copy(ptrNew,ptrOrigin) then	-- Otherwise just copy the Tag and Value
							doReportWarning(strTag,fhGetValueAsText(ptrOrigin),"Value not accepted",IntHelpCheck)
						end
					end
				end
			end
			doCopyChildrenItems(ptrOrigin,ptrNew)

		end -- local function doCopyChildBranch

		intTag = intTag + 1																			-- Compile table of New Tag Data Reference hierarchy
		tblTag[intTag] = fhGetTag(ptrNew)

		local ptrOrigin = fhNewItemPtr()
		ptrOrigin = ptrOld:Clone()
		ptrOrigin:MoveToFirstChildItem(ptrOrigin)
		while ptrOrigin:IsNotNull() do
			doCopyChildBranch(ptrOrigin,ptrNew)													-- Copy any Child Branch data
			ptrOrigin:MoveNext()
		end

		tblTag[intTag] = nil
		intTag = intTag - 1

	end -- local function doCopyChildrenItems

	-- Delete Item and Report if Fails --
	local function doDeleteItem(ptrTag,strVal,strFunc)
		local strTag = fhGetTag(ptrTag)
		if not fhDeleteItem(ptrTag) then
			doReportWarning(strTag,strVal,"fhDeleteItem failed in "..strFunc,IntHelpReport)
		end
	end -- local function doDeleteItem

	-- Copy the Tag Value --
	local function flgCopyTagValue()
		local strTag = fhGetTag(ptrOld)
		local strVal = fhGetValueAsText(ptrOld)
		local ptrLnk = fhGetValueAsLink(ptrOld)
		local intVal = fhGetValueAsInteger(ptrOld)
		local dtDate = fhGetValueAsDate(ptrOld)
		local strOldText = fhGetItemText(ptrOld,strTag)
		local strOldType = fhGetValueType(ptrOld)												-- Type strings "", "text", "richtext", "link", "integer", "date"
		local strOldClas = fhGetDataClass(ptrOld)
		local strNewType = fhGetValueType(ptrNew)
		local strNewClas = fhGetDataClass(ptrNew)

		-- 	If converting Event to Attribute is there an _UNCAT value that can be used as Attribute value	-- V3.3
		if fhIsEvent(ptrOld) and fhIsAttribute(ptrNew) then
			local strUncat = fhGetItemText(ptrOld,"~._UNCAT")
			if #strUncat > 0 then
				strVal = strUncat
				strOldText = strUncat
				strOldType = "text"
				local ptrUncat = fhGetItemPtr(ptrOld,"~._UNCAT") 
				fhDeleteItem(ptrUncat)
			end
		end

		-- Omit all Event/RESIdence Facts where strVal == strOldType == ""
		if strOldType ~= "" then

			if strOldType == "text" then															-- Attribute Fact or UDF Tag
				if strVal == "" then
					return true
				end
				local strLnk																			-- Try to convert old Tag text Value into a Link Pointer
				ptrLnk,strLnk = PtrStrTranslateLink(strVal)
				if ptrLnk:IsNotNull() then
					strOldType = "link"
					strTag = strLnk
				else
					intVal = tonumber(strVal)														-- Try to convert old Tag text Value into an Integer
					if intVal ~= nil
					and strNewType == "integer" then
						strOldType = "integer"
					elseif dtDate:SetValueAsText(strVal)											-- Try to convert old Tag text Value into a Date Object
					and strNewType == "date" then
						strOldType = "date"
					end
				end
			end

			if strOldType == "link" and ( fhIsAttribute(ptrNew) or fhIsEvent(ptrNew) ) then
				local ptrNew = fhCreateItem(strTag,ptrNew)										-- Create a Fact child SOUR/OBJE/NOTE Record link
				if ptrNew:IsNull() then
					doReportWarning(strTag,strVal,"Invalid Tag ~ Tag Skipped",IntHelpCheck)
					return false
				else
					if not fhSetValueAsLink(ptrNew,ptrLnk) then									-- Set the Link Pointer as its Value
						doReportWarning(strTag,strVal,"Invalid Link ~ Tag Skipped",IntHelpCheck)
						doDeleteItem(ptrNew,strVal,"flgCopyTagValue")
						return false
					end
				end
			elseif strNewType == "text" then														-- Set new Tag value to old Tag value converted to Text
				if strOldType == "richtext" then													-- V3.3
					strOldText = fhGetValueAsRichText(ptrOld):GetPlainText()
				end
				if not fhSetValueAsText(ptrNew,strOldText) then
					doReportWarning(strTag,strOldText,"Invalid Text for "..strNew.." ~ Tag Skipped",IntHelpCheck)
					return false
				end
			elseif strNewType == "richtext" then													-- Set new Tag value to old Tag value converted to Rich Text -- V3.3
				local strRichText = fhNewRichText()
				if strOldType == "richtext" then
					strRichText = fhGetValueAsRichText(ptrOld)
				else
					strRichText:SetText(strOldText)
				end
				if not fhSetValueAsRichText(ptrNew,strRichText) then
					doReportWarning(strTag,strOldText,"Invalid Text for "..strNew.." ~ Tag Skipped",IntHelpCheck)
					return false
				end
			elseif strNewType == "" then															-- New Tag is an Event, or RESIdence, so create SOURce Note for Value
				local ptrVal = fhNewItemPtr()
				ptrVal = fhCreateItem("SOUR2",ptrNew)
				if ptrVal:IsNull() then
					doReportWarning(strTag,strOldText,"Invalid Tag ~ Tag Skipped",IntHelpCheck)
					return false
				else																					-- Set new SOURce Note value to old Tag name and value converted to Text
					if not fhSetValueAsText(ptrVal,StrOldData.."="..strOldText) then
						doReportWarning(strTag,strOldText,"Invalid Text ~ Tag Skipped",IntHelpCheck)
						doDeleteItem(ptrVal,strOldText,"flgCopyTagValue")
						return false
					end
				end
			elseif strNewType == strOldType then
				if	strNewType == "link" then														-- Set the Link Pointer as current Tag value
					if not fhSetValueAsLink(ptrNew,ptrLnk) then
						doReportWarning(strTag,strOldText,"Invalid Link for "..strNew.." ~ Tag Skipped",IntHelpCheck)
						return false
					end
				elseif strNewType == "integer" then												-- Set the Integer Value as current Tag value
					if not fhSetValueAsInteger(ptrNew,intVal) then
						doReportWarning(strTag,strOldText,"Invalid Integer for "..strNew.." ~ Tag Skipped",IntHelpCheck)
						return false
					end
				elseif strNewType == "date" then													-- Set the Date Object as current Tag value
					if not fhSetValueAsDate(ptrNew,dtDate) then
						doReportWarning(strTag,strOldText,"Invalid Date for "..strNew.." ~ Tag Skipped",IntHelpCheck)
						return false
					end
				else
					doReportWarning(strTag,strOldText,"Invalid Tag Type: "..strNewType.." ~ Tag Skipped",IntHelpReport)
					return false
				end
			else
				if not fhSetValue_Copy(ptrNew,ptrOld) then										-- Expected to fail
					doReportWarning(strTag,strVal,"Invalid value for "..strNew.." ~ Tag Skipped",IntHelpCheck)
					return false
				end
			end
		end
		return true
	end -- local function flgCopyTagValue

	if strNew == StrDeleteTag then																-- Delete the old Tag from Record
		WriteDataChange(ptrRecord,ptrOld,strNew,StrDeleteTag)
		doDeleteItem(ptrOld,StrDeleteTag,"DoEditDataDetails")
		return isCont																					-- Continue after Warning?	-- V3.8
	end

	ptrNew = fhCreateItem(strNew,ptrRecord)														-- Ensure the New Tag has been created
	if ptrNew:IsNull() then
		doReportWarning(strNew,TblDictionary[fhGetTag(ptrRecord)][strNew],"Cannot be created ~ Tag Skipped",IntHelpCheck)
	else
		if not fhMoveItemBefore(ptrNew,ptrOld) then												-- Ensure the Tag order is maintained
			doReportWarning(strNew,TblDictionary[fhGetTag(ptrRecord)][strNew],"Cannot be moved ~ Tag Skipped",IntHelpCheck)
			doDeleteItem(ptrNew,"MoveItemBefore","DoEditDataDetails")
		else
			if not flgCopyTagValue() then															-- Transfer old Tag value to new Tag value, or new SOURce Note
				doDeleteItem(ptrNew,"CopyTagValue","DoEditDataDetails")
			else
				doCopyChildrenItems(ptrOld,ptrNew)												-- Copy the Child Tag structure

				WriteDataChange(ptrRecord,ptrOld,ptrNew,"")										-- Log the Change

				doDeleteItem(ptrOld,"CopyTagValue","DoEditDataDetails")						-- Delete the Old Tag from Record

				for intItem, strLog in ipairs(tblLog) do
					WriteLog("Warning\t"..strLog)													-- Log each Warning Message
					local ptrVal = fhNewItemPtr()
					ptrVal = fhCreateItem("SOUR2",ptrNew)										-- Copy each Warning into Tag SOURce Note
					if ptrVal:IsNull() then
						ptrVal = fhCreateItem("SOUR2",ptrRecord)								-- or into whole Record SOURce Note
						if ptrVal:IsNull() then
							ptrVal = fhCreateItem("NOTE2",ptrRecord)							-- or into whole Record local NOTE
							if ptrVal:IsNull() then
								doReportWarning("SOUR2/NOTE2","strLog","Warning cannot be created",IntHelpReport)
								return
							end
						end
					end
					if not fhSetValueAsText(ptrVal,strLog) then									-- Save the Warning in Source/Local Note
						doReportWarning("SOUR2/NOTE2",strLog,"Warning cannot be saved",IntHelpReport)
					end
				end

				return isCont																			-- Continue after Warning?	-- V3.8

			end
		end
	end

	WriteDataChange(ptrRecord,ptrOld,ptrNew,StrSkipTag)										-- Log the Skip

	for intItem, strLog in ipairs(tblLog) do
		WriteLog("Warning\t"..strLog)																-- Log each Warning Message
	end

	return isCont																						-- Continue after Warning?	-- V3.8

end -- function DoEditDataDetails

-- Check if Filters Match the Selected Tag --
function FlgFiltersMatch(ptrTag,tblFilter)
	local flgFilter -- Determines whether Plain=true or Pattern=false filter matching
	if StrPattern == "OFF" then flgFilter = true else flgFilter = false end

	-- Check if Filter Value matches Record Value and fail if no match
	if string.find(fhGetItemText(ptrTag,fhGetTag(ptrTag)),tblFilter[IntFilterValue],1,flgFilter) == nil then return false end

	local strMatchDate  = tblFilter[IntFilterDate]
	local flgMatchDate  -- Determines if Filter Date matches a DATE field
	local strMatchPlace = tblFilter[IntFilterPlace]
	local flgMatchPlace -- Determines if Filter Place matches a PLAC field
	-- An empty Filter always matches, else if no field exists then it never matches
	if strMatchDate  == "" then flgMatchDate  = true else flgMatchDate  = false end
	if strMatchPlace == "" then flgMatchPlace = true else flgMatchPlace = false end

	if not ( flgMatchDate and flgMatchPlace ) then
		-- Search the Child Tags for other Filters
		local ptrChild = fhNewItemPtr()
		ptrChild:MoveToFirstChildItem(ptrTag)
		while ptrChild:IsNotNull() do
			local strTag = fhGetTag(ptrChild)	-- V2.6
			if strTag == "DATE" and not flgMatchDate then
				-- Check if Filter Date matches DATE value and fail if no match
				if  string.find(fhGetItemText(ptrChild,"DATE:COMPACT"),strMatchDate,1,flgFilter) == nil -- V2.5 added :COMPACT
				and string.find(fhGetItemText(ptrChild,"DATE:LONG"),strMatchDate,1,flgFilter) == nil then return false end
				flgMatchDate = true
			end
			if strTag:match("^_?PLAC$") and not flgMatchPlace then
				-- Check if Filter Place matches PLACe/_PLACe value and fail if no match	-- V2.6 cater for _PLAC
				if  string.find(fhGetValueAsText(ptrChild),strMatchPlace,1,flgFilter) == nil
				and string.find(fhGetValueAsText(fhGetItemPtr(ptrTag,"~._PLAC")),strMatchPlace,1,flgFilter) == nil then return false end	-- V2.6 _PLAC
				flgMatchPlace = true
			end
			ptrChild:MoveNext()
		end
		if not ( flgMatchDate and flgMatchPlace ) then
			-- DATE or PLAC not matched at Child level so check Grandchild level
			ptrChild:MoveToFirstChildItem(ptrTag)
			while ptrChild:IsNotNull() do
				local ptrGrandChild = fhNewItemPtr()
				ptrGrandChild:MoveToFirstChildItem(ptrChild)
				while ptrGrandChild:IsNotNull() do
					local strTag = fhGetTag(ptrGrandChild) -- V2.6
					if strTag == "DATE" and not flgMatchDate then
						-- Check if Filter Date matches DATE value and fail if no match
						if  string.find(fhGetItemText(ptrGrandChild,"DATE:COMPACT"),strMatchDate,1,flgFilter) == nil -- V2.5 added :COMPACT
						and string.find(fhGetItemText(ptrGrandChild,"DATE:LONG"),strMatchDate,1,flgFilter) == nil then return false end
						flgMatchDate = true
					end
					if strTag == "PLAC" and not flgMatchPlace then
						-- Check if Filter Place matches PLACe value and fail if no match
						if string.find(fhGetValueAsText(ptrGrandChild),strMatchPlace,1,flgFilter) == nil then return false end
						flgMatchPlace = true
					end
					ptrGrandChild:MoveNext()
				end
				ptrChild:MoveNext()
			end
		end
	end
	return flgMatchDate and flgMatchPlace
end -- function FlgFiltersMatch

-- Change Selected Tag --
function DoChangeData(strRecTag,strOldSet,strNewSet,strOldTag,strNewTag,tblFilter)
	-- strRecTag	~ "INDI","FAM","SOUR",etc record type of Tag to change/delete
	-- strOldSet	~ Source Tag Set of Source Tag to change from or delete
	-- strNewSet	~ Target Tag Set of Target Tag to change into
	-- strOldTag	~ Source Tag Name to change from, or to delete 
	-- strNewTag	~ Target Tag Name to change into, or StrDeleteTag to delete Source
	-- tblFilter	~ List of Filter Value, Date, Place text strings, and Records

	-- Iterate selected or all Records --														-- V2.9
	local function ptrRecords(strRecTag,arrRecords)
		local ptrRec = fhNewItemPtr()
		local ptrNxt = fhNewItemPtr()
		local intRec
		if #arrRecords > 0 then
			intRec = 1
			ptrRec = arrRecords[intRec]															-- 1st selected record
		else
			ptrRec:MoveToFirstRecord(strRecTag)													-- 1st of all records
		end
		return function ()
			ptrNxt = ptrRec:Clone()																	-- Next record to return
			if intRec then
				intRec = intRec + 1																	-- Next selected record
				ptrRec = arrRecords[intRec] or fhNewItemPtr()
			else
				ptrRec:MoveNext()																	-- Next of all records
			end
			if ptrNxt:IsNotNull() then return ptrNxt end
		end
	end -- local function ptrRecords

	-- Translate Tag to its Name --
	local function strTranslateTag(strTag)
		local strData
		if strTag == StrDeleteTag then return strTag end
		strData = TblDictionary[strRecTag][strTag]
		strData = strData:replace("_ATTR-","")
		strData = strData:replace("FACT-","")													-- FH V7	-- V3.1
		strData = strData:replace("EVEN-","")
		return strData
	end -- local function strTranslateTag

	StrOldData = strTranslateTag(strOldTag)
	StrNewData = strTranslateTag(strNewTag)

	strOldTag = strOldTag:gsub("<h>$","")														-- Fix <hidden> fact issue -- V4.1
	strNewTag = strNewTag:gsub("<h>$","")

	local flgContinue= true																			-- Set false by Warning dialogue Abort Changes button	-- V3.8
	local flgConvert = true																			-- Enable the Tag change/delete conversion operations
	local flgConfirm = ( StrConfirm == "ON" )													-- Enable/disable the GUI Data confirmation dialogue
	local ptrOldTag = fhNewItemPtr()																-- Pointer to source Tag
	local ptrGotTag = fhNewItemPtr()																-- Pointer to current Tag
	local ptrNewTag = fhNewItemPtr()																-- Pointer to target Tag
	local ptrRecord = fhNewItemPtr()																-- Pointer to current Record

	local intTagTotal = 0																			-- Total matching source Tags
	for ptrRecord in ptrRecords(strRecTag,tblFilter[IntFilterRecord]) do					-- V2.9
		ptrOldTag:MoveTo(ptrRecord,"~."..strOldTag)
		while ptrOldTag:IsNotNull() do
			intTagTotal = intTagTotal + 1															-- Count matching source Tags
			ptrOldTag:MoveNext("SAME_TAG")
		end
	end
	if intTagTotal > 100 and not flgConfirm then
		progbar.Start("Editing Tags",intTagTotal)												-- Optionally start Progress Bar
	end
	local intTagCount = intTagTotal																-- Count pending source Tags

	IntSkippedData = 0																				-- Count skipped source Tags
	IntDeletedData = 0																				-- Count deleted source Tags
	IntChangedData = 0																				-- Count changed source Tags
	IntWarningData = 0																				-- Count warning Messages

	if tblFilter[IntFilterValue]..tblFilter[IntFilterDate]..tblFilter[IntFilterPlace] ~= "" then
		WriteLog("Filters\tValue="..tblFilter[IntFilterValue].."    Date="..tblFilter[IntFilterDate].."    Place="..tblFilter[IntFilterPlace])
	end

	for ptrRecord in ptrRecords(strRecTag,tblFilter[IntFilterRecord]) do					-- V2.9
		ptrOldTag:MoveTo(ptrRecord,"~."..strOldTag)
		while ptrOldTag:IsNotNull() do
			ptrGotTag = ptrOldTag:Clone()															-- Clone got Tag from old Tag
			ptrOldTag:MoveNext("SAME_TAG")
			if FlgFiltersMatch(ptrGotTag,tblFilter) then
				if flgConfirm then																	-- Prompt user to choose action
					flgConvert,flgConfirm = GUI_DataDialogue(ptrRecord,ptrGotTag,strNewTag)
					if not flgConfirm then
						if not flgConvert then break end											-- Cancel all edits
						if intTagCount > 100 then													-- Optionally start Progress Bar
							progbar.Start("Editing Tags",intTagCount)							-- V3.8
						end
					end
				end
				if flgConvert then																	-- Change/Delete the old Tag in the Record
					flgContinue = DoEditDataDetails(ptrRecord,ptrGotTag,strNewTag) 			-- V3.8
				else																					-- Skip the old Tag in the Record
					WriteDataChange(ptrRecord,ptrGotTag,ptrNewTag,StrSkipTag)
				end
			end
			intTagCount = intTagCount - 1															-- Move onto next Tag
			progbar.Step(1)
			progbar.Message(strRecTag.." Rec Id "..fhGetRecordId(ptrRecord))
			collectgarbage("step",0)																-- Improves run time	-- V3.4
		end
		if progbar.Stop() then break end															-- Cancel all edits?
		if not flgConfirm and not flgConvert then break end
		if not flgContinue then break end															-- V3.8
	end
	progbar.Close()
	local strOldData = strOldTag..": "..StrOldData
	local strSkipped = ""
	local strMessage = ""
	if intTagCount > 0 then																			-- Some items were skipped by cancelling
		IntSkippedData = IntSkippedData + intTagCount
		IntSkippedTotal = IntSkippedTotal + intTagCount
		strSkipped = "Edit cancelled and "..intTagCount.." '"..strOldData.."' items skipped."
		if intTagCount == 1 then strSkipped = strSkipped:gsub(" items "," item ") end
		WriteLog("\t"..strSkipped)
		strSkipped = strSkipped.."\n\n"
	end
	if IntSkippedData + IntDeletedData + IntChangedData == 0 then
		strMessage = "No '"..strOldData.."' items edited."
	else
		strMessage = "Finished '"..strOldData.."' item editing."
	end
	WriteLog("\t"..strMessage)

	iup_gui.CustomDialogue("Memo","0x0")															-- Custom "Memo" dialogue minimum size & centralised
	iup_gui.MemoDialogue(strSkipped..strMessage.."\n"..UpdateCountStatus())

	general.SaveStringToFile(table.concat(ArrLogFileText,"\r\n"),StrLogFilePath)			--	Save Log File	-- V3.7

	if IntChangedData > 0 then																		-- Insert the New Tag into the Old Tag index and update dropdown lists
		DoInsertTagIndex(strRecTag,strNewSet,strNewTag,StrModeOld)
		StrPopulateDropdownLists(strRecTag)														-- 17 Aug 2013
	end
	if ( IntDeletedData + IntChangedData ) == intTagTotal then
		-- Remove the Old Tag from the Old Tag index and update dropdown lists
		DoRemoveTagIndex(strRecTag,strOldSet,strOldTag)
		StrPopulateDropdownLists(strRecTag)														-- 17 Aug 2013
	end
end -- function DoChangeData

-- Populate the Record Type dropdown list --
function StrPopulateRecordDropList()
	-- Preserve currently selected Record Type
	local strOldRec = iup.GetAttribute(LstRecords,tostring(LstRecords.Value))
	local intOldRec = 0
	LstRecords.RemoveItem = nil
	for intRecTag,strRecTag in ipairs(TblRecordTag) do
	  if strRecTag ~= "INDI" and strRecTag ~= "FAM" then
		for intItem,strSet in ipairs(TblSetNameIndex[strRecTag]) do
			if FlgTableHasData(TblOldTagIndex[strRecTag][strSet]) then
				local strNewRec = TblRecordName[intRecTag]
				LstRecords.AppendItem = strNewRec													-- Add the Record Type because it has associated Set names with associated Tags
				intOldRec = intOldRec + 1
				if strNewRec == strOldRec then
					LstRecords.Value = intOldRec													-- Select this Record Type if it was selected before
				end																						-- Works because TblRecordTag determines order in LstRecords which is not sorted
				break
			end
		end
	  end
	end
	if LstRecords.Value == "0" then LstRecords.Value = 1 end
	return TblDictionary[iup.GetAttribute(LstRecords,tostring(LstRecords.Value))]
end -- function StrPopulateRecordDropList

-- Populate any pair of Tag Set dropdown lists --
function StrStrPopulateSetDropList(strRecTag,lstOldDrop,lstNewDrop)
	-- Preserve currently selected Set Names
	local strOldSet = iup.GetAttribute(lstOldDrop,tostring(lstOldDrop.Value))
	local strNewSet = iup.GetAttribute(lstNewDrop,tostring(lstNewDrop.Value))
	local intOldSet = 0
	local intNewSet = 0
	lstOldDrop.RemoveItem = nil
	lstNewDrop.RemoveItem = nil
	for intItem,strSet in ipairs(TblSetNameIndex[strRecTag]) do
		if FlgTableHasData(TblOldTagIndex[strRecTag][strSet]) then
			lstOldDrop.AppendItem = strSet														-- Add the Set because it has associated Tags
			intOldSet = intOldSet + 1
			if strSet == strOldSet then
				lstOldDrop.Value = intOldSet														-- Select this Set if it was selected before
			end																							-- Works because TblSetNameIndex determines order in lstOldDrop which is not sorted
		end
		if	FlgTableHasData(TblNewTagIndex[strRecTag][strSet]) and strSet ~= StrUDFTagsSet then
			lstNewDrop.AppendItem = strSet														-- Add the Set because it has associated Tags, unless it is the UDF Tags Set 
			intNewSet = intNewSet + 1
			if strSet == strNewSet then
				lstNewDrop.Value = intNewSet														-- Select this Set if it was selected before
			end																							-- Works because TblSetNameIndex determines order in lstNewDrop which is not sorted
		end
	end
	if lstOldDrop.Value == "0" then lstOldDrop.Value = 1 end
	if lstNewDrop.Value == "0" then lstNewDrop.Value = 1 end
	return iup.GetAttribute(lstOldDrop,tostring(lstOldDrop.Value)),iup.GetAttribute(lstNewDrop,tostring(lstNewDrop.Value))
end -- function StrStrPopulateSetDropList

-- Populate any Tag Name dropdown list --														-- V3.7
function DoPopulateNameDropList(strRecTag,tblTagIndex,strSetName,lstDrop)
	-- Preserve currently selected Tag Name
	local strOldTag = iup.GetAttribute(lstDrop,tostring(lstDrop.Value))
	local intOldTag = 0
	lstDrop.RemoveItem = nil
	for intTag, strTag in ipairs(tblTagIndex[strSetName]) do
		local strNewTag = TblDictionary[strRecTag][strTag]
		lstDrop.AppendItem = strNewTag
		intOldTag = intOldTag + 1
	end
	for intOldTag = 1, intOldTag do
		if strOldTag == iup.GetAttribute(lstDrop,tostring(intOldTag)) then
			lstDrop.Value = intOldTag																-- Select this Tag Name if it was selected before
			break																						-- Need this technique because tblTagIndex is not same order as lstDrop sorted order
		end
	end
																										-- Leave no Tag Name selected if nothing has matched
end -- function DoPopulateNameDropList

																										-- Fix initial setup problem -- V4.1
local strIndOldSet = nil																			-- Individual Old Tag Set chosen
local strIndNewSet = nil																			-- Individual New Tag Set chosen
local strFamOldSet = nil																			-- Family Old Tag Set chosen
local strFamNewSet = nil																			-- Family New Tag Set chosen
local strRecOldSet = nil																			-- Record Old Tag Set chosen
local strRecNewSet = nil																			-- Record New Tag Set chosen

-- Populate the Dropdown lists on current Tab --												-- Fix initial setup problem -- V4.1
function StrPopulateDropdownLists(strRecTag)													-- 17 Aug 2013 intChosenTab > strRecTag
	if strRecTag == "INDI" then																	-- Individual Records Tab
		-- Add each Fact Set name to Individual dropdown list
		strIndOldSet, strIndNewSet = StrStrPopulateSetDropList(strRecTag,LstIndOldSet,LstIndNewSet)
		-- Add each Fact Name to Individual dropdown lists
		DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strIndOldSet,LstIndOldTag)
		DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strIndNewSet,LstIndNewTag)
	elseif strRecTag == "FAM" then																-- Family Records Tab
		-- Add each Fact Set name to Family dropdown list
		strFamOldSet, strFamNewSet = StrStrPopulateSetDropList(strRecTag,LstFamOldSet,LstFamNewSet)
		-- Add each Fact Name to Family dropdown lists
		DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strFamOldSet,LstFamOldTag)
		DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strFamNewSet,LstFamNewTag)
	elseif type(strRecTag) == "string" then														-- Other Records Tab
		strRecTag = StrPopulateRecordDropList()
		-- Add each Tag Set name to Record dropdown lists
		strRecOldSet, strRecNewSet = StrStrPopulateSetDropList(strRecTag,LstRecOldSet,LstRecNewSet)
		-- Add each Tag Name to Record dropdown lists
		DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strRecOldSet,LstRecOldTag)
		DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strRecNewSet,LstRecNewTag)
	end
	return strRecTag
end -- function StrPopulateDropdownLists

-- GUI View Log File Dialogue --
function GUI_LogsDialogue(tblLogFile)

	-- Create each GUI text and button with title, etc
	local txtView	= iup.text	{ ReadOnly="YES"; MultiLine="YES"; AutoHide="YES"; VisibleColumns="52"; VisibleLines="10"; }
	local btnOldest	= iup.button	{ Title="Oldest Log File"; }
	local btnPrior	= iup.button	{ Title="Prior Log File" ; }
	local btnDelete	= iup.button	{ Title="Delete Log File"; }
	local btnNext	= iup.button	{ Title="Next Log File"  ; }
	local btnLatest	= iup.button	{ Title="Latest Log File"; }
	local btnClose	= iup.button	{ Title="Close View && Delete Log Files"; Expand="NO"; }

	-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
	local dialogLogs = iup.dialog { Title=StrPluginTitle.." View & Delete Log Files";
								iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Margin;
									iup.frame { Font=iup_gui.FontHead; Title="View & Delete Log Files";
										iup.vbox {
											txtView;
											iup.hbox { Expand="HORIZONTAL"; Homogeneous="YES"; btnOldest; btnPrior; btnDelete; btnNext; btnLatest; Margin=iup_gui.Margin; };
											iup.hbox { Expand="HORIZONTAL"; Homogeneous="YES"; iup.fill{}; btnClose; iup.fill{}; };
										};
									};
								};
							}

	local intLogFile = tblLogFile[0]																-- Number of log files

	local function doNothing()																		-- Inhibit general error message	-- V3.7
	end

	local function doDeleteLogFile()																-- Action for Delete Log File button
		if not general.DeleteFile(tblLogFile[intLogFile],doNothing) then					-- V3.7
			iup.Message(StrPluginTitle, "Old log file not deleted!")
		else
			if tblLogFile[intLogFile] == StrLogFilePath then
				LblLogFile.Title = "Log File not created."										-- Current log file deleted so update Main GUI
				LogFileHandle = nil
			end
			table.remove(tblLogFile,intLogFile)													-- Remove log file name from table
			tblLogFile[0] = tblLogFile[0] - 1														-- Decrement number of log file names
		end
	end -- local function doDeleteLogFile

	local function doViewLogFile(strLogFile)													-- Display Log File contents in multiline text box -- V3.7
		txtView.Value = general.StrLoadFromFile(strLogFile)
	end -- local function doViewLogFile

	local function doUpdateDisplay()																-- Update buttons and multiline text box
		if tblLogFile[0] <= 0 then
			tblLogFile[0] = 0																		-- No log files exist, so all buttons inactive
			intLogFile = 0
			btnOldest.Active	= "NO"
			btnPrior.Active	= "NO"
			btnDelete.Active	= "NO"
			btnNext.Active	= "NO"
			btnLatest.Active	= "NO"
			txtView.Value		= "No Log Files"
		elseif tblLogFile[0] == 1 then															-- One log file exists, so only Delete button active
			intLogFile = 1
			btnOldest.Active	= "NO"
			btnPrior.Active	= "NO"
			btnDelete.Active	= "YES"
			btnNext.Active	= "NO"
			btnLatest.Active	= "NO"
			doViewLogFile(tblLogFile[intLogFile])
		else
			btnOldest.Active	= "YES"
			btnPrior.Active	= "YES"
			btnDelete.Active	= "YES"
			btnNext.Active	= "YES"
			btnLatest.Active	= "YES"
			if intLogFile == 1 then																	-- Oldest log file selected, so cannot go backward
				btnOldest.Active	= "NO"
				btnPrior.Active	= "NO"
			elseif intLogFile >= tblLogFile[0] then												-- Latest log file selected, so cannot go forward
				intLogFile = tblLogFile[0]
				btnNext.Active	= "NO"
				btnLatest.Active	= "NO"
			end
			doViewLogFile(tblLogFile[intLogFile])
		end
	end -- local function doUpdateDisplay

	-- Set other GUI control attributes
	local tblControls={ { "Font"; "FgColor"; "Tip"; "action"; {"TipBalloon";"Balloon";}; {"Expand";"YES";}; doUpdateDisplay; };
		[txtView]   = { "FontBody"; "Body"; "Selected log file details"	; false; };
		[btnOldest] = { "FontBody"; "Safe"; "Select oldest log file"	; function() intLogFile=1					doUpdateDisplay() end; };
		[btnPrior]  = { "FontBody"; "Safe"; "Select prior log file"		; function() intLogFile=intLogFile-1		doUpdateDisplay() end; };
		[btnDelete] = { "FontBody"; "Warn"; "Delete selected log file"	; function() doDeleteLogFile()			doUpdateDisplay() end; };
		[btnNext]   = { "FontBody"; "Safe"; "Select next log file"		; function() intLogFile=intLogFile+1		doUpdateDisplay() end; };
		[btnLatest] = { "FontBody"; "Safe"; "Select latest log file"	; function() intLogFile=tblLogFile[0]	doUpdateDisplay() end; };
		[btnClose]  = { "FontBody"; "Risk"; "Close this View and Delete Log Files window"; function() return iup.CLOSE end; };
		}

	iup_gui.AssignAttributes(tblControls)														-- Assign GUI control attributes

	iup_gui.ShowDialogue("Logs",dialogLogs,btnClose,"normal keep")

end -- function GUI_LogsDialogue

-- GUI Warnings Dialogue --
function GUI_WarnDialogue(strName,strData,strWarn,intHelp)

	local isCont	= true 																			-- V3.8
	local tblHelp = {"Check that the Source and Target are compatible";"Data value is logged in a Source Note";"Please report via FHUG Forum > Plugins Discussion";}

	-- Create each GUI label and button with title, etc
	local lblNameHead = iup.label	{ Title="Name: "   ; Alignment="ARIGHT"; Size="50"; }
	local lblNameText = iup.label	{ Title=strName; }
	local lblDataHead = iup.label	{ Title="Data: "   ; Alignment="ARIGHT"; Size="50"; }
	local lblDataText = iup.label	{ Title=strData; }
	local lblWarnHead = iup.label	{ Title="Warning: "; Alignment="ARIGHT"; Size="50"; }
	local lblWarnText = iup.label	{ Title=strWarn; }
	local lblHelpText = iup.label	{ Title=tblHelp[intHelp]; Alignment="ACENTER"; }
	local btnAbort    = iup.button	{ Title="Abort Changes"; action=function() isCont = false	return iup.CLOSE end; }	-- V3.8
	local btnClose    = iup.button	{ Title="Close Warning"; action=function() isCont = true		return iup.CLOSE end; }	-- V3.8

	-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
	local dialogWarn	= iup.dialog { Title=StrPluginTitle.." Warning";
									iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Border;
										iup.frame { Font=iup_gui.FontHead; FgColor=iup_gui.Head; Title="Warning";
											iup.vbox { Margin=iup_gui.Margin;
												iup.hbox { lblNameHead; lblNameText; };
												iup.hbox { lblDataHead; lblDataText; };
												iup.hbox { lblWarnHead; lblWarnText; };
												lblHelpText;
												iup.hbox { btnAbort; btnClose; };					-- V3.8
											};
										};
									};
								}

	-- Set other GUI control attributes
	local tblControls={ { "Font"; "FgColor"; "Expand"; "Tip"; {"TipBalloon";"Balloon";}; };
		[lblNameHead] = { "FontBody"; "Body"; "VERTICAL"  ; "Record name"; };
		[lblNameText] = { "FontBody"; "Body"; "HORIZONTAL"; "Record name"; };
		[lblDataHead] = { "FontBody"; "Body"; "VERTICAL"  ; "Record data"; };
		[lblDataText] = { "FontBody"; "Body"; "HORIZONTAL"; "Record data"; };
		[lblWarnHead] = { "FontBody"; "Risk"; "VERTICAL"  ; "Warning message"; };
		[lblWarnText] = { "FontBody"; "Risk"; "HORIZONTAL"; "Warning message"; };
		[lblHelpText] = { "FontBody"; "Safe"; "YES"       ; "Advice message" ; };
		[btnAbort]    = { "FontBody"; "Risk"; "YES"       ; "Abort all these Changes"; };
		[btnClose]    = { "FontBody"; "Risk"; "YES"       ; "Close this Warning window"; };
		}

	iup_gui.AssignAttributes(tblControls)														-- Assign GUI control attributes

	iup_gui.ShowDialogue("Memo",dialogWarn,btnClose,"normal keep")

	return isCont																						-- V3.8

end -- function GUI_WarnDialogue

-- GUI Data Dialogue --
function GUI_DataDialogue(ptrRecord,ptrOldData,strNewTag)

	local bEdit = false																				-- true means Edit Tags & false means Skip Tags
	local bConf = true																				-- true means confirm next Tag Edit & false means do the rest
	local strCol = "Body"																			-- Colour of lblEditHead/Text depends on strEditWarn
	local strEditText	= ""																		-- Text statement of options 
	local strEditWarn	= ""																		-- Warning message makes text red
	local strEditData	= ""																		-- Delete/Change this Tag button label
	local strEditRest	= ""																		-- Delete/Change the Rest button label
	local strOldType		= ""																		-- Source Tag type is "Event" or "Attribute" or "Tag"
	local strNewType		= ""																		-- Target Tag type is "Event" or "Attribute" or "Tag"
	local ptrNewData		= fhNewItemPtr()

	-- Determine if Fact Event or Fact Attribute or Data Tag --
	local function strDataType(ptrData)
		local strType
		if	fhIsAttribute(ptrData) then
			strType = "Attribute"
		elseif fhIsEvent(ptrData) then
			strType = "Event"
		elseif fhIsUDF(ptrData) then
			if			StrOldData:sub(-5) == "Event"		then	strType = "Event"
			elseif		StrOldData:sub(-9) == "Attribute"	then	strType = "Attribute"
			else														strType = "Tag"  
			end
		else
			strType = "Tag"
		end
		return strType
	end -- local function strDataType

	strOldType = strDataType(ptrOldData)
	if strNewTag == StrDeleteTag then
		strEditText = "Delete this "..strOldType.." ?"..strEditText
		strEditData = "Delete this "..strOldType
		strEditRest = "Delete all the Rest"
	else
		ptrNewData = fhCreateItem(strNewTag,ptrRecord)
		strNewType = strDataType(ptrNewData)
		fhDeleteItem(ptrNewData)
		if strOldType == "Attribute" and fhGetTag(ptrOldData) ~= "RESI" and ( strNewType == "Event" or strNewTag == "RESI" ) then
			strEditWarn = "\nAny Attribute value will migrate to a SOURce Note !"
			strCol = "Risk"
		end
		strEditText = "Change this "..strOldType.." to "..strNewTag..": "..StrNewData.." "..strNewType.." ?"..strEditWarn..strEditText
		strEditData = "Change to "..StrNewData.." "..strNewType
		strEditRest = "Change all the Rest"
	end

	-- Create each GUI label and button with title, etc
	local lblNameHead = iup.label	{ Title="Name: "  ; Alignment="ARIGHT"; Size="50"; }
	local lblNameText = iup.label	{ Title=StrDisplayName(ptrRecord); }
	local lblDataHead = iup.label	{ Title="Data: "  ; Alignment="ARIGHT"; Size="50"; }
	local lblDataText = iup.label	{ Title=StrDisplayData(ptrOldData); }
	local lblEditHead = iup.label	{ Title="Option: "; Alignment="ARIGHT"; Size="50"; }
	local lblEditText = iup.label	{ Title=strEditText; }
	local btnEditData = iup.button{ Title=strEditData; }
	local btnSkipData = iup.button{ Title="Skip this "..strOldType; }
	local btnEditRest = iup.button{ Title=strEditRest; }
	local btnSkipRest = iup.button{ Title="Skip all the Rest"; }

	-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
	local dialogData = iup.dialog { Title=StrPluginTitle.." Confirm Edit";
									iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Border;
										iup.frame { Font=iup_gui.FontHead; FgColor=iup_gui.Head; Title="Confirm Edit";
											iup.vbox { Margin=iup_gui.Margin;
												iup.hbox { lblNameHead; lblNameText; };
												iup.hbox { lblDataHead; lblDataText; };
												iup.hbox { lblEditHead; lblEditText; };
												iup.hbox { btnEditData; btnSkipData; Homogeneous="YES"; };
												iup.hbox { btnEditRest; btnSkipRest; Homogeneous="YES"; };
											};
										};
									};
									close_cb=function() bEdit=false bConf=false end;			-- Skip all Tag changes
								}

	-- Set other GUI control attributes
	local tblControls={ { "Font"; "FgColor"; "Expand"; "Tip"; "action"; {"TipBalloon";"Balloon";}; };
		[lblNameHead] = { "FontBody"; "Body"; "VERTICAL"  ; "Fact/Tag name"; };
		[lblNameText] = { "FontBody"; "Body"; "HORIZONTAL"; "Fact/Tag name"; };
		[lblDataHead] = { "FontBody"; "Body"; "VERTICAL"  ; "Fact/Tag data"; };
		[lblDataText] = { "FontBody"; "Body"; "HORIZONTAL"; "Fact/Tag data"; };
		[lblEditHead] = { "FontBody"; strCol; "VERTICAL"  ; "Fact/Tag edit"; };
		[lblEditText] = { "FontBody"; strCol; "HORIZONTAL"; "Fact/Tag edit"; };
		[btnEditData] = { "FontBody"; "Safe"; "YES"       ; "Edit this Fact/Tag"     ; function() bEdit=true  bConf=true  return iup.CLOSE end; };
		[btnSkipData] = { "FontBody"; "Safe"; "YES"       ; "Skip this Fact/Tag"     ; function() bEdit=false bConf=true  return iup.CLOSE end; };
		[btnEditRest] = { "FontBody"; "Safe"; "YES"       ; "Edit rest of Facts/Tags"; function() bEdit=true  bConf=false return iup.CLOSE end; };
		[btnSkipRest] = { "FontBody"; "Safe"; "YES"       ; "Skip rest of Facts/Tags"; function() bEdit=false bConf=false return iup.CLOSE end; };
		}

	iup_gui.AssignAttributes(tblControls)														-- Assign GUI control attributes

	iup_gui.ShowDialogue("Memo",dialogData,btnSkipRest,"normal keep")

	return bEdit,bConf
end -- function GUI_DataDialogue

-- GUI Main Dialogue --
function GUI_MainDialogue()
	local tblSet			= TblSetNameIndex														-- V3.7
	local flgQuitMode	= false																	-- btnQuit.action() = true, and btnClose.action() = false
	local strRecTag		= "INDI"																	-- Current Record Tag "INDI", "FAM", "NOTE", "SOUR", etc
	local strIndOldTag	= nil																		-- Individual Old Tag Name to be removed
	local strIndNewTag	= nil																		-- Individual New Tag Name to be inserted
	local tblIndFilter	= {"";"";"";{};}															-- Individual Old Tag Filters for Value, Date, Place, Records	-- V2.9
	local strFamOldTag	= nil																		-- Family Old Tag Name to be removed
	local strFamNewTag	= nil																		-- Family New Tag Name to be inserted
	local tblFamFilter	= {"";"";"";{};}															-- Family Old Tag Filters for Value, Date, Place, Records	-- V2.9
	local strRecOldTag	= nil																		-- Record Old Tag Name to be removed
	local strRecNewTag	= nil																		-- Record New Tag Name to be inserted
	local tblRecFilter	= {"";"";"";{};}															-- Record Old Tag Filters for Value, Date, Place, Records	-- V2.9
	local strIndDelete	= " Delete every 'Source Tag Name' chosen above "
	local strIndChange	= " Change 'Source Tag Name' into 'Target Tag Name' "
	local strFamDelete	= " Delete every 'Source Tag Name' chosen above "
	local strFamChange	= " Change 'Source Tag Name' into 'Target Tag Name' "
	local strRecDelete	= " Delete every 'Source Tag Name' chosen above "
	local strRecChange	= " Change 'Source Tag Name' into 'Target Tag Name' "

	-- If a button title is too long, then truncate long names and add elipsis
	local function strButtonTitle(strRecTag,strOldTag,strMid,strNewTag)
		local strOld = ""
		local strNew = ""
		if strOldTag then 																			-- V2.5	-- V4.2 -- Cater for <hidden> default tags CHR, BAPM, ANUL, DIV
			strOld = TblDictionary[strRecTag][strOldTag.."<h>"] or TblDictionary[strRecTag][strOldTag]
		end
		if strNewTag then																			-- V2.5	-- V4.2
			strNew = TblDictionary[strRecTag][strNewTag.."<h>"] or TblDictionary[strRecTag][strNewTag]
		end
		if ( #strOld + #strMid + #strNew ) > IntMaxNameLen * 2 - 4 then
			if #strOld > IntMaxNameLen then
				strOld = strOld:sub(1,IntMaxNameLen-3).."..."
			end
			if #strNew > IntMaxNameLen then
				strNew = strNew:sub(1,IntMaxNameLen-3).."..."
			end
		end
		return strOld..strMid..strNew
	end -- local function strButtonTitle

	local function iupRadio(tglA,tglB)															-- Return radio handle created from two toggles
		return iup.radio { iup.hbox { tglA; tglB; Homogeneous="YES"; }; Value=tglA; }
	end -- local function iupRadio

	-- Create the Individual Records controls with titles/values and tooltips, etc
	local btnCHR_BAPM	= iup.button	{ Title=strButtonTitle("INDI","CHR"," to ","BAPM"); }
	local btnBAPM_CHR	= iup.button	{ Title=strButtonTitle("INDI","BAPM"," to ","CHR"); }
	local lblIndOldSet	= iup.label	{ Title="  Source Tag Set:" ; Alignment="ARIGHT"; Size="80"; }
			LstIndOldSet	= iup.list	{ DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; }
	local lblIndNewSet	= iup.label	{ Title="  Target Tag Set:" ; Alignment="ARIGHT"; Size="80"; }
			LstIndNewSet	= iup.list	{ DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; }
	local lblIndOldTag	= iup.label	{ Title="  Source Tag Name:"; Alignment="ARIGHT"; Size="80"; }
			LstIndOldTag	= iup.list	{ DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; }
	local lblIndNewTag	= iup.label	{ Title="  Target Tag Name:"; Alignment="ARIGHT"; Size="80"; }
			LstIndNewTag	= iup.list	{ DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; }
	local btnIndDelete	= iup.button	{ Title=strIndDelete; Active="NO"; }
	local btnIndChange	= iup.button	{ Title=strIndChange; Active="NO"; }
	local lblIndValue	= iup.label	{ Title=" Value:"; }
	local txtIndValue	= iup.text	{ Value=tblIndFilter[IntFilterValue]; }
	local lblIndDate		= iup.label	{ Title=" Date:"; }
	local txtIndDate		= iup.text	{ Value=tblIndFilter[IntFilterDate]; }
	local lblIndPlace	= iup.label	{ Title=" Place:"; }
	local txtIndPlace	= iup.text	{ Value=tblIndFilter[IntFilterPlace]; }
	local lblIndRecord	= iup.label	{ Title=" Records: "; }
	local tglIndRecAll	= iup.toggle	{ Title="All"; }											-- V2.9
	local tglIndRecSel	= iup.toggle	{ Title="Pick"; }										-- V2.9
	local radIndRecord	= iupRadio	( tglIndRecAll, tglIndRecSel )						-- V2.9

	-- Create the Family Records controls with titles/values and tooltips, etc
	local btnDIV_ANUL	= iup.button	{ Title=strButtonTitle("FAM","DIV"," to ","ANUL"); }
	local btnANUL_DIV	= iup.button	{ Title=strButtonTitle("FAM","ANUL"," to ","DIV"); }
	local lblFamOldSet	= iup.label	{ Title="  Source Tag Set:" ; Alignment="ARIGHT"; Size="80"; }
			LstFamOldSet	= iup.list	{ DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; }
	local lblFamNewSet	= iup.label	{ Title="  Target Tag Set:" ; Alignment="ARIGHT"; Size="80"; }
			LstFamNewSet	= iup.list	{ DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; }
	local lblFamOldTag	= iup.label	{ Title="  Source Tag Name:"; Alignment="ARIGHT"; Size="80"; }
			LstFamOldTag	= iup.list	{ DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; }
	local lblFamNewTag	= iup.label	{ Title="  Target Tag Name:"; Alignment="ARIGHT"; Size="80"; }
			LstFamNewTag	= iup.list	{ DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; }
	local btnFamDelete	= iup.button	{ Title=strFamDelete; Active="NO"; }
	local btnFamChange	= iup.button	{ Title=strFamChange; Active="NO"; }
	local lblFamValue	= iup.label	{ Title=" Value:"; }
	local txtFamValue	= iup.text	{ Value=tblFamFilter[IntFilterValue]; }
	local lblFamDate		= iup.label	{ Title=" Date:"; }
	local txtFamDate		= iup.text	{ Value=tblFamFilter[IntFilterDate]; }
	local lblFamPlace	= iup.label	{ Title=" Place:"; }
	local txtFamPlace	= iup.text	{ Value=tblFamFilter[IntFilterPlace]; }
	local lblFamRecord	= iup.label	{ Title=" Records: "; }
	local tglFamRecAll	= iup.toggle	{ Title="All"; }											-- V2.9
	local tglFamRecSel	= iup.toggle	{ Title="Pick"; }										-- V2.9
	local radFamRecord	= iupRadio	( tglFamRecAll, tglFamRecSel )						-- V2.9

	-- Create the Other Records controls with titles/values and tooltips, etc
	local lblRecords		= iup.label	{ Title="  Record Type:"    ; Alignment="ARIGHT"; Size="80"; }
			LstRecords	= iup.list	{ DropDown="YES"; Value="1" ; VisibleColumns="9"; Visible_Items="16"; }
	local lblRecOldSet	= iup.label	{ Title="  Source Tag Set:" ; Alignment="ARIGHT"; Size="80"; }
			LstRecOldSet	= iup.list	{ DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; }
	local lblRecNewSet	= iup.label	{ Title="  Target Tag Set:" ; Alignment="ARIGHT"; Size="80"; }
			LstRecNewSet	= iup.list	{ DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; }
	local lblRecOldTag	= iup.label	{ Title="  Source Tag Name:"; Alignment="ARIGHT"; Size="80"; }
			LstRecOldTag	= iup.list	{ DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; }
	local lblRecNewTag	= iup.label	{ Title="  Target Tag Name:"; Alignment="ARIGHT"; Size="80"; }
			LstRecNewTag	= iup.list	{ DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; }
	local btnRecDelete	= iup.button	{ Title=strRecDelete; Active="NO"; }
	local btnRecChange	= iup.button	{ Title=strRecChange; Active="NO"; }
	local lblRecValue	= iup.label	{ Title=" Value:"; }
	local txtRecValue	= iup.text	{ Value=tblRecFilter[IntFilterValue]; }
	local lblRecDate		= iup.label	{ Title=" Date:"; }
	local txtRecDate		= iup.text	{ Value=tblRecFilter[IntFilterDate]; }
	local lblRecPlace	= iup.label	{ Title=" Place:"; }
	local txtRecPlace	= iup.text	{ Value=tblRecFilter[IntFilterPlace]; }
	local lblRecRecord	= iup.label	{ Title=" Records: "; }
	local tglRecRecAll	= iup.toggle	{ Title="All"; }											-- V2.9
	local tglRecRecSel	= iup.toggle	{ Title="Pick"; }										-- V2.9
	local radRecRecord	= iupRadio	( tglRecRecAll, tglRecRecSel )						-- V2.9

	-- Create the Status & Settings controls with titles/values and tooltips, etc
	local lblCounts		= iup.label	{ Title=" Data Counts: ";	Alignment="ARIGHT"; }
			LblCoSkipped	= iup.label	{ Title="   0 Skipped. ";	Alignment="ARIGHT"; }
			LblCoDeleted	= iup.label	{ Title="   0 Deleted. ";	Alignment="ARIGHT"; }
			LblCoChanged	= iup.label	{ Title="   0 Changed. ";	Alignment="ARIGHT:ABOTTOM"; }
			LblCoWarning	= iup.label	{ Title="   0 Warnings.";	Alignment="ARIGHT"; }
	local lblTotals		= iup.label	{ Title=" Data Totals: ";	Alignment="ARIGHT"; }
			LblToSkipped	= iup.label	{ Title="   0 Skipped. ";	Alignment="ARIGHT"; }
			LblToDeleted	= iup.label	{ Title="   0 Deleted. ";	Alignment="ARIGHT"; }
			LblToChanged	= iup.label	{ Title="   0 Changed. ";	Alignment="ARIGHT"; }
			LblToWarning	= iup.label	{ Title="   0 Warnings.";	Alignment="ARIGHT"; }
	local tglLogNeed		= iup.toggle	{ Title=StrLogNeedTitle.." ?"; }
	local tglPattern		= iup.toggle	{ Title=StrPatternTitle.." ?"; }
	local tglConfirm		= iup.toggle	{ Title=StrConfirmTitle.." ?"; }
	local tglWarning		= iup.toggle	{ Title=StrWarningTitle.." ?"; }
			LblLogFile	= iup.label	{ Title="Log File not created."; WordWrap="YES"; Alignment=":ATOP"; Size="90x16"; }
	local btnViewLog		= iup.button	{ Title=" View && Delete Log Files"	}
	local btnUpdates		= iup.button	{ Title="Check for Updates"; }						-- V4.0
	local btnDefault		= iup.button	{ Title="Restore Default Settings"		}
	local btnSetFont		= iup.button	{ Title="Set Window Fonts"				}
	local btnQuit			= iup.button	{ Title="QUIT Plugin and UNDO Updates"	}
	local btnGetHelp		= iup.button	{ Title=" Obtain Help && Advice"		}
	local btnClose		= iup.button	{ Title="CLOSE Plugin and KEEP Updates"}

	local intTabPosn	=	0																			-- GUI Tab currently displayed, 0=Undefined, 1=Individual, 2=Family, 3=Other, 4=Status

	-- Create the Individual Records box
	local headIndiv		=	iup.label { Title=" ~ Choose an Individual Record Fact/Tag to Edit ~ "; }
	local vboxIndiv		=	iup.vbox { Alignment="ACENTER"; Margin="0x0"; headIndiv;
									iup.hbox { Homogeneous="YES"; iup.hbox { iup.label{Size="80";};btnCHR_BAPM;}	; iup.hbox { iup.label{Size="80";};btnBAPM_CHR;}; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblIndOldSet; LstIndOldSet; }		; iup.hbox { lblIndNewSet; LstIndNewSet; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblIndOldTag; LstIndOldTag; }		; iup.hbox { lblIndNewTag; LstIndNewTag; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { btnIndDelete; }							; iup.hbox { btnIndChange; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblIndValue; txtIndValue; }			; iup.hbox { lblIndDate; txtIndDate; }; iup.hbox { lblIndPlace; txtIndPlace; }; iup.hbox { lblIndRecord; radIndRecord; }; };	-- V2.9
								}

	-- Create the Family Records box
	local headFamily	=	iup.label { Title=" ~ Choose a Family Record Fact/Tag to Edit ~ "; }
	local vboxFamily	=	iup.vbox { Alignment="ACENTER"; Margin="0x0"; headFamily;
									iup.hbox { Homogeneous="YES"; iup.hbox { iup.label{Size="80";};btnDIV_ANUL;}	; iup.hbox { iup.label{Size="80";};btnANUL_DIV; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblFamOldSet; LstFamOldSet; }		; iup.hbox { lblFamNewSet; LstFamNewSet; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblFamOldTag; LstFamOldTag; }		; iup.hbox { lblFamNewTag; LstFamNewTag; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { btnFamDelete; }							; iup.hbox { btnFamChange; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblFamValue; txtFamValue; }			; iup.hbox { lblFamDate; txtFamDate; }; iup.hbox { lblFamPlace; txtFamPlace; }; iup.hbox { lblFamRecord; radFamRecord; }; };	-- V2.9
								}

	-- Create the Other Records box
	local headRecord	=	iup.label { Title=" ~ Choose a Record Type Gedcom Tag to Edit ~ "; }
	local vboxRecord	=	iup.vbox { Alignment="ACENTER"; Margin="0x0"; headRecord;
									iup.hbox { Homogeneous="YES"; iup.hbox { lblRecords ; LstRecords; }			; iup.hbox { }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblRecOldSet; LstRecOldSet; }		; iup.hbox { lblRecNewSet; LstRecNewSet; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblRecOldTag; LstRecOldTag; }		; iup.hbox { lblRecNewTag; LstRecNewTag; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { btnRecDelete; }							; iup.hbox { btnRecChange; }; };
									iup.hbox { Homogeneous="YES"; iup.hbox { lblRecValue; txtRecValue; }			; iup.hbox { lblRecDate; txtRecDate; }; iup.hbox { lblRecPlace; txtRecPlace; }; iup.hbox { lblRecRecord; radRecRecord; }; };	-- V2.9
								}

	-- Create the Status box
	local headStatus	=	iup.label { Title=" ~ Status and Settings Information ~ "; }
			HboxCounts=	iup.hbox { Homogeneous="YES"; lblCounts; LblCoSkipped; LblCoDeleted; LblCoChanged; LblCoWarning; iup.label { }; }
	local hboxTotals	=	iup.hbox { Homogeneous="YES"; lblTotals; LblToSkipped; LblToDeleted; LblToChanged; LblToWarning; iup.label {visible="NO";}; }
	local vboxStatus	=	iup.vbox { Alignment="ACENTER"; Margin="2x0"; headStatus;
									iup.vbox { Homogeneous="YES";
										HboxCounts;
										hboxTotals;
										iup.hbox { Homogeneous="YES"; tglLogNeed; tglPattern; tglConfirm; tglWarning; };
									};
									iup.hbox { Homogeneous="YES"; LblLogFile; };
									iup.hbox { Homogeneous="YES"; btnViewLog; btnUpdates; btnDefault; btnSetFont; Margin="0x0"; };	-- V4.0
								}

	-- Create the Tab controls
	local tabControls	=	iup.tabs {
									vboxIndiv;	TabTitle0="Individual Records";
									vboxFamily;	TabTitle1="Family Records";
									vboxRecord;	TabTitle2="Other Records";
									vboxStatus;	TabTitle3="Status && Settings  ";
									tabchangepos_cb=function(self,intNewTab,intOldTab) 	intTabPosn=intNewTab+1 if intTabPosn <= 3 then strRecTag=StrPopulateDropdownLists(TblRecordTag[intTabPosn]) end end;	-- 17 Aug 2013 -- 28 Dec 2013
								}

	-- Create dialogue and turn off resize, menubox, maximize and minimize
	local headMain		=	iup.label { Alignment="ACENTER:ACENTER"; Title="Please only use this plugin on a copy of your data until you are satisfied with the results."; }
	local vboxMain		=	iup.vbox  { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Margin;
									headMain;
									tabControls;
									iup.hbox { Homogeneous="YES"; btnQuit; btnGetHelp; btnClose; };
								}

	local dialogMain	=	iup.dialog { Title=StrPluginTitle..iup_gui.Version;
									vboxMain;
									close_cb=function() if vboxMain.Active == "NO" then return iup.IGNORE else return iup.CLOSE end end;
								}

	if fhGetAppVersion() > 6 then																	-- FH V7 IUP 3.28	-- V3.1
		tabControls.TabPadding="10x7"
	else																								-- FH V6 IUP 3.11	-- V3.1
		tabControls.Padding="10x7"
	end

	local function doChangeData(...)																-- Disable GUI while changing data
		vboxMain.Active = "NO"
		DoChangeData(...)
		vboxMain.Active = "YES"
	end -- local function doChangeData

	local function setControls()																	-- Set GUI control values
		tglLogNeed.Value = StrLogNeed
		tglPattern.Value = StrPattern
		tglConfirm.Value = StrConfirm
		tglWarning.Value = StrWarning
		tblIndFilter[IntFilterRecord] = {}														-- Reset the Records filters	-- V2.9
		tglIndRecAll.Value = "ON"
		tblFamFilter[IntFilterRecord] = {}
		tglFamRecAll.Value = "ON"
		tblRecFilter[IntFilterRecord] = {}
		tglRecRecAll.Value = "ON"
	end -- local function setControls

	local strTipDelete = "Delete selected Source Facts/Tags"
	local strTipChange = "Change selected Source Facts/Tags to Target Facts/Tags"
	local strTipOldSet = "Select source Fact/Tag Set for the list below"
	local strTipNewSet = "Select target Fact/Tag Set for the list below"
	local strTipOldTag = "Select source Fact/Tag Name from the Set above"
	local strTipNewTag = "Select target Fact/Tag Name from the Set above"
	local strTipValue  = "Filter source on Attribute Value"
	local strTipDate   = "Filter source on Fact/Tag Date"
	local strTipPlace  = "Filter source on Fact/Tag Place"
	local strTipRecord = "Filter source by Records ?"											-- V2.9
	local strTipRecAll = "Do not filter source by Records"									-- V2.9
	local strTipRecSel = "Filter source by selected Records"									-- V2.9

 	local tblState = { "OFF"; "ON"; }																-- Toggle control settings per state

	-- Set other GUI control attributes, but see later for further action functions
	local tblControls={{ "Font"; "FgColor"; "Expand"; "Tip"; "action"; {"TipBalloon";"Balloon"}; setControls; };
	-- Individual Records
		[headIndiv]   = { "FontHead"; "Head"; "VERTICAL"; };
		[vboxIndiv]   = { "FontBody"; "Body"; false; };
		[btnCHR_BAPM] = { "FontBody"; "Safe"; "YES"; "Change Christening to Baptism Events"; function() doChangeData("INDI",StrStandEvents,StrStandEvents,"CHR","BAPM",tblIndFilter) end; };
		[btnBAPM_CHR] = { "FontBody"; "Safe"; "YES"; "Change Baptism to Christening Events"; function() doChangeData("INDI",StrStandEvents,StrStandEvents,"BAPM","CHR",tblIndFilter) end; };
		[lblIndOldSet]= { "FontBody"; "Body"; false; strTipOldSet; };
		[LstIndOldSet]= { "FontBody"; "Safe"; "YES"; strTipOldSet; };
		[lblIndNewSet]= { "FontBody"; "Body"; false; strTipNewSet; };
		[LstIndNewSet]= { "FontBody"; "Safe"; "YES"; strTipNewSet; };
		[lblIndOldTag]= { "FontBody"; "Body"; false; strTipOldTag; };
		[LstIndOldTag]= { "FontBody"; "Safe"; "YES"; strTipOldTag; };
		[lblIndNewTag]= { "FontBody"; "Body"; false; strTipNewTag; };
		[LstIndNewTag]= { "FontBody"; "Safe"; "YES"; strTipNewTag; };
		[btnIndDelete]= { "FontBody"; "Safe"; "YES"; strTipDelete; };
		[btnIndChange]= { "FontBody"; "Safe"; "YES"; strTipChange; };
		[lblIndValue] = { "FontBody"; "Body"; false; strTipValue ; };
		[txtIndValue] = { "FontBody"; "Safe"; "YES"; strTipValue ; };
		[lblIndDate]  = { "FontBody"; "Body"; false; strTipDate  ; };
		[txtIndDate]  = { "FontBody"; "Safe"; "YES"; strTipDate  ; };
		[lblIndPlace] = { "FontBody"; "Body"; false; strTipPlace ; };
		[txtIndPlace] = { "FontBody"; "Safe"; "YES"; strTipPlace ; };
		[lblIndRecord]= { "FontBody"; "Body"; false; strTipRecord; };						-- V2.9
		[tglIndRecAll]= { "FontBody"; "Body"; "YES"; strTipRecAll; };						-- V2.9
		[tglIndRecSel]= { "FontBody"; "Body"; "YES"; strTipRecSel; };						-- V2.9
	-- Family Records
		[headFamily]  = { "FontHead"; "Head"; "VERTICAL"; };
		[vboxFamily]  = { "FontBody"; "Body"; false; };
		[btnDIV_ANUL] = { "FontBody"; "Safe"; "YES"; "Change Divorce to Annulment Events"; function() doChangeData("FAM",StrStandEvents,StrStandEvents,"DIV","ANUL",tblFamFilter) end; };
		[btnANUL_DIV] = { "FontBody"; "Safe"; "YES"; "Change Annulment to Divorce Events"; function() doChangeData("FAM",StrStandEvents,StrStandEvents,"ANUL","DIV",tblFamFilter) end; };
		[lblFamOldSet]= { "FontBody"; "Body"; false; strTipOldSet; };
		[LstFamOldSet]= { "FontBody"; "Safe"; "YES"; strTipOldSet; };
		[lblFamNewSet]= { "FontBody"; "Body"; false; strTipNewSet; };
		[LstFamNewSet]= { "FontBody"; "Safe"; "YES"; strTipNewSet; };
		[lblFamOldTag]= { "FontBody"; "Body"; false; strTipOldTag; };
		[LstFamOldTag]= { "FontBody"; "Safe"; "YES"; strTipOldTag; };
		[lblFamNewTag]= { "FontBody"; "Body"; false; strTipNewTag; };
		[LstFamNewTag]= { "FontBody"; "Safe"; "YES"; strTipNewTag; };
		[btnFamDelete]= { "FontBody"; "Safe"; "YES"; strTipDelete; };
		[btnFamChange]= { "FontBody"; "Safe"; "YES"; strTipChange; };
		[lblFamValue] = { "FontBody"; "Body"; false; strTipValue ; };
		[txtFamValue] = { "FontBody"; "Safe"; "YES"; strTipValue ; };
		[lblFamDate]  = { "FontBody"; "Body"; false; strTipDate  ; };
		[txtFamDate]  = { "FontBody"; "Safe"; "YES"; strTipDate  ; };
		[lblFamPlace] = { "FontBody"; "Body"; false; strTipPlace ; };
		[txtFamPlace] = { "FontBody"; "Safe"; "YES"; strTipPlace ; };
		[lblFamRecord]= { "FontBody"; "Body"; false; strTipRecord; };						-- V2.9
		[tglFamRecAll]= { "FontBody"; "Body"; "YES"; strTipRecAll; };						-- V2.9
		[tglFamRecSel]= { "FontBody"; "Body"; "YES"; strTipRecSel; };						-- V2.9
	-- Other Records
		[headRecord]  = { "FontHead"; "Head"; "VERTICAL"; };
		[vboxRecord]  = { "FontBody"; "Body"; false; };
		[lblRecords]  = { "FontBody"; "Body"; false; "Select record type"; };
		[LstRecords]  = { "FontBody"; "Safe"; "YES"; "Select record type"; };
		[lblRecOldSet]= { "FontBody"; "Body"; false; strTipOldSet:gsub("Fact/",""); };
		[LstRecOldSet]= { "FontBody"; "Safe"; "YES"; strTipOldSet:gsub("Fact/",""); };
		[lblRecNewSet]= { "FontBody"; "Body"; false; strTipNewSet:gsub("Fact/",""); };
		[LstRecNewSet]= { "FontBody"; "Safe"; "YES"; strTipNewSet:gsub("Fact/",""); };
		[lblRecOldTag]= { "FontBody"; "Body"; false; strTipOldTag:gsub("Fact/",""); };
		[LstRecOldTag]= { "FontBody"; "Safe"; "YES"; strTipOldTag:gsub("Fact/",""); };
		[lblRecNewTag]= { "FontBody"; "Body"; false; strTipNewTag:gsub("Fact/",""); };
		[LstRecNewTag]= { "FontBody"; "Safe"; "YES"; strTipNewTag:gsub("Fact/",""); };
		[btnRecDelete]= { "FontBody"; "Safe"; "YES"; strTipDelete:gsub("Facts/",""); };
		[btnRecChange]= { "FontBody"; "Safe"; "YES"; strTipChange:gsub("Facts/",""); };
		[lblRecValue] = { "FontBody"; "Body"; false; strTipValue:gsub("Attribute","Tag"); };
		[txtRecValue] = { "FontBody"; "Safe"; "YES"; strTipValue:gsub("Attribute","Tag"); };
		[lblRecDate]  = { "FontBody"; "Body"; false; strTipDate :gsub("Fact/",""); };
		[txtRecDate]  = { "FontBody"; "Safe"; "YES"; strTipDate :gsub("Fact/",""); };
		[lblRecPlace] = { "FontBody"; "Body"; false; strTipPlace:gsub("Fact/",""); };
		[txtRecPlace] = { "FontBody"; "Safe"; "YES"; strTipPlace:gsub("Fact/",""); };
		[lblRecRecord]= { "FontBody"; "Body"; false; strTipRecord; };						-- V2.9
		[tglRecRecAll]= { "FontBody"; "Body"; "YES"; strTipRecAll; };						-- V2.9
		[tglRecRecSel]= { "FontBody"; "Body"; "YES"; strTipRecSel; };						-- V2.9
	-- Status & Settings
		[headStatus]  = { "FontHead"; "Head"; "VERTICAL"; };
		[vboxStatus]  = { "FontBody"; "Body"; false; };
		[lblCounts]   = { "FontBody"; "Body"; "YES"; "Counts for the last edit"; };
		[LblCoSkipped]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags skipped by last edit"; };
		[LblCoDeleted]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags deleted by last edit"; };
		[LblCoChanged]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags changed by last edit"; };
		[LblCoWarning]= { "FontBody"; "Body"; "YES"; "Number of Fact/Tag warnings for last edit"; };
		[lblTotals]   = { "FontBody"; "Body"; "YES"; "Totals for this session"; };
		[LblToSkipped]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags skipped this session"; };
		[LblToDeleted]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags deleted this session"; };
		[LblToChanged]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags changed this session"; };
		[LblToWarning]= { "FontBody"; "Body"; "YES"; "Number of Fact/Tag warnings this session"; };
		[tglLogNeed]  = { "FontBody"; "Body"; "YES"; "Choose whether to log edits to file"												; function(self,intState) StrLogNeed = tblState[intState+1] SaveSettings() end; };
		[tglPattern]  = { "FontBody"; "Body"; "YES"; "Choose to Filter with Regular Expression style Patterns, or Plain Text"	; function(self,intState) StrPattern = tblState[intState+1] SaveSettings() end; };
		[tglConfirm]  = { "FontBody"; "Body"; "YES"; "Choose whether to confirm Fact/Tag edits"										; function(self,intState) StrConfirm = tblState[intState+1] SaveSettings() end; };
		[tglWarning]  = { "FontBody"; "Body"; "YES"; "Choose whether to show warning messages"											; function(self,intState) StrWarning = tblState[intState+1] SaveSettings() end; };
		[LblLogFile]  = { "FontBody"; "Body"; "YES"; "Log file name (when created)"; };
		[btnViewLog]  = { "FontBody"; "Safe"; "YES"; "View and optionally delete all log files"; };
		[btnUpdates]  = { "FontBody"; "Safe"; "YES"; "Check for a later plugin version in the Plugin Store"; }; 	-- V4.0
		[btnDefault]  = { "FontBody"; "Safe"; "YES"; "Restore default Settings for Window sizes and positions, \n"..StrLogNeedTitle..", "..StrPatternTitle..", "..StrConfirmTitle..", and "..StrWarningTitle; };
		[btnSetFont]  = { "FontBody"; "Safe"; "YES"; "Choose user interface window font styles"; };
	-- Main Controls
		[headMain]    = { "FontHead"; "Risk"; "VERTICAL"; };
		[vboxMain]    = { "FontHead"; "Head"; false; };
		[tabControls] = { "FontHead"; "Head"; false; "Choose between Individual Records, Family Records, or \nOther Records such as Note, Source, Repository, etc, \nor Status and Settings Information."; };
		[btnQuit]     = { "FontBody"; "Risk"; "YES"; "Quit the plugin and undo any updates"												; function() flgQuitMode=true	return iup.CLOSE end; };
		[btnGetHelp]  = { "FontBody"; "Safe"; "YES"; "Obtain online Help and Advice from the Plugin Store"; };
		[btnClose]    = { "FontBody"; "Risk"; "YES"; "Close the plugin and keep all updates"											; function()							return iup.CLOSE end; };
		}

	-- GUI Control Functions --

	local function doUpdateButtons(strRecord,strOldTag,strNewTag,btnChange,strChange,btnDelete)
		if strOldTag and strNewTag and strNewTag ~= strOldTag then
			btnChange.Active = "YES"																-- Enable & label Change button if Source & Target Tag Names OK
			btnChange.Title = strButtonTitle(strRecord,strOldTag," to ",strNewTag)
		else
			btnChange.Active = "NO"																	-- Disable & label Change button
			btnChange.Title = strChange
		end
		if btnDelete and strOldTag then
			btnDelete.Active = "YES"																-- Enable & label Delete button if Source Tag Name OK
			btnDelete.Title = strButtonTitle(strRecord,nil,"Delete every ",strOldTag)
		end
	end -- local function doUpdateButtons

	local function doIndButtonsReset(strMode)													-- Reset the Delete & Change buttons and Tag Name lists
		-- strMode = StrModeAll if either button is pressed
		-- strMode = StrModeOld if 'Source Tag Set' is changed
		-- strMode = StrModeNew if 'Target Tag Set' is changed
		btnIndChange.Title = strIndChange
		btnIndChange.Active = "NO"
		if strMode == StrModeOld or strMode == StrModeAll then
			btnIndDelete.Title = strIndDelete
			btnIndDelete.Active = "NO"
			LstIndOldTag.Value = 0
			strIndOldTag = nil
		end
		if strMode == StrModeNew or strMode == StrModeAll then
			LstIndNewTag.Value = 0
			strIndNewTag = nil
		end
	end -- local function doIndButtonsReset

	local function getRecordFilter(strType)														-- Prompt user for Record selection -- V2.9
		dialogMain.Active="No"																		-- V3.8
		local arrRec = fhPromptUserForRecordSel(strType)
		if #arrRec == 0 then																		-- Cancelled so use current selection
			arrRec = fhGetCurrentRecordSel(strType)
		end
		dialogMain.Active="Yes"																		-- V3.8
		return arrRec
	end -- local function getRecordFilter

	function btnIndDelete:action()																-- Action for Delete fact button
		doChangeData("INDI",strIndOldSet,"",strIndOldTag,StrDeleteTag,tblIndFilter)
		doIndButtonsReset(StrModeAll)
	end

	function btnIndChange:action()																-- Action for Change facts button
		doChangeData("INDI",strIndOldSet,strIndNewSet,strIndOldTag,strIndNewTag,tblIndFilter)
		doIndButtonsReset(StrModeAll)
	end

	function LstIndOldSet:action(strText,intItem,iState)										-- Action for Source Tag Set dropdown
		if iState == 1 then
			LstIndOldSet.Value = intItem
			local indoldsetval = LstIndOldSet.Value												-- Populate Source Tag Name dropdown
			strIndOldSet = strText
			DoPopulateNameDropList(strRecTag,TblOldTagIndex["INDI"],strIndOldSet,LstIndOldTag)
			doIndButtonsReset(StrModeOld)
		end
	end

	function LstIndNewSet:action(strText,intItem,iState)										-- Action for Target Tag Set dropdown
		if iState == 1 then
			LstIndNewSet.Value = intItem
			local indnewsetval = LstIndNewSet.Value												-- Populate Target Tag Name dropdown
			strIndNewSet = strText
			DoPopulateNameDropList(strRecTag,TblNewTagIndex["INDI"],strIndNewSet,LstIndNewTag)
			doIndButtonsReset(StrModeNew)
		end
	end -- function LstIndNewSet:action

	function LstIndOldTag:action(strText,intItem,iState)										-- Action for Source Tag Name dropdown
		if iState == 1 then
			strIndOldTag = TblDictionary["INDI"][strIndOldSet][strText]						-- V2.5
			doUpdateButtons("INDI",strIndOldTag,strIndNewTag,btnIndChange,strIndChange,btnIndDelete)
		end
	end

	function LstIndNewTag:action(strText,intItem,iState)										-- Action for Target Tag Name dropdown
		if iState == 1 then
			strIndNewTag = TblDictionary["INDI"][strIndNewSet][strText]						-- V2.5
			doUpdateButtons("INDI",strIndOldTag,strIndNewTag,btnIndChange,strIndChange)
		end
	end

	function txtIndValue:valuechanged_cb()														-- Call back when Filter Value is changed
		tblIndFilter[IntFilterValue] = txtIndValue.Value
	end

	function txtIndDate:valuechanged_cb()														-- Call back when Filter Date is changed
		tblIndFilter[IntFilterDate] = txtIndDate.Value
	end

	function txtIndPlace:valuechanged_cb()														-- Call back when Filter Place is changed
		tblIndFilter[IntFilterPlace] = txtIndPlace.Value
	end

	function tglIndRecAll:action(intState)														-- Action for Record All toggle -- V2.9
		if intState == 1 then
			tblIndFilter[IntFilterRecord] = {}
		end
	end

	function tglIndRecSel:action(intState)														-- Action for Record Pick toggle -- V2.9
		if intState == 1 then
			tblIndFilter[IntFilterRecord] = getRecordFilter("INDI")
		end
	end

	local function doFamButtonsReset(strMode)													-- Reset the Delete & Change buttons and Tag Name lists
		-- strMode = StrModeAll if either button is used
		-- strMode = StrModeOld if 'Source Tag Set' is changed
		-- strMode = StrModeNew if 'Target Tag Set' is changed
		btnFamChange.Title = strFamChange
		btnFamChange.Active = "NO"
		if strMode == StrModeOld or strMode == StrModeAll then
			btnFamDelete.Title = strFamDelete
			btnFamDelete.Active = "NO"
			LstFamOldTag.Value = 0
			strFamOldTag = nil
		end
		if strMode == StrModeNew or strMode == StrModeAll then
			LstFamNewTag.Value = 0
			strFamNewTag = nil
		end
	end -- local function doFamButtonsReset

	function btnFamDelete:action()																-- Action for Delete fact button
		doChangeData("FAM",strFamOldSet,"",strFamOldTag,StrDeleteTag,tblFamFilter)
		doFamButtonsReset(StrModeAll)
	end

	function btnFamChange:action()																-- Action for Change facts button
		doChangeData("FAM",strFamOldSet,strFamNewSet,strFamOldTag,strFamNewTag,tblFamFilter)
		doFamButtonsReset(StrModeAll)
	end

	function LstFamOldSet:action(strText,intItem,iState)										-- Action for Source Tag Set dropdown
		if iState == 1 then
			strFamOldSet = strText																	-- Populate Source Tag Name dropdown
			DoPopulateNameDropList(strRecTag,TblOldTagIndex["FAM"],strFamOldSet,LstFamOldTag)
			doFamButtonsReset(StrModeOld)
		end
	end

	function LstFamNewSet:action(strText,intItem,iState)										-- Action for Target Tag Set dropdown
		if iState == 1 then
			strFamNewSet = strText																	-- Populate Target Tag Name dropdown
			DoPopulateNameDropList(strRecTag,TblNewTagIndex["FAM"],strFamNewSet,LstFamNewTag)
			doFamButtonsReset(StrModeNew)
		end
	end

	function LstFamOldTag:action(strText,intItem,iState)										-- Action for Source Tag Name dropdown
		if iState == 1 then
			strFamOldTag = TblDictionary["FAM"][strFamOldSet][strText]						-- V2.5
			doUpdateButtons("FAM",strFamOldTag,strFamNewTag,btnFamChange,strFamChange,btnFamDelete)
		end
	end

	function LstFamNewTag:action(strText,intItem,iState)										-- Action for Target Tag Name dropdown
		if iState == 1 then
			strFamNewTag = TblDictionary["FAM"][strFamNewSet][strText]						-- V2.5
			doUpdateButtons("FAM",strFamOldTag,strFamNewTag,btnFamChange,strFamChange)
		end
	end

	function txtFamValue:valuechanged_cb()														-- Call back when Filter Value is changed
		tblFamFilter[IntFilterValue] = txtFamValue.Value
	end

	function txtFamDate:valuechanged_cb()														-- Call back when Filter Date is changed
		tblFamFilter[IntFilterDate] = txtFamDate.Value
	end

	function txtFamPlace:valuechanged_cb()														-- Call back when Filter Place is changed
		tblFamFilter[IntFilterPlace] = txtFamPlace.Value
	end

	function tglFamRecAll:action(intState)														-- Action for Record All toggle -- V2.9
		if intState == 1 then
			tblFamFilter[IntFilterRecord] = {}
		end
	end

	function tglFamRecSel:action(intState)														-- Action for Record Pick toggle -- V2.9
		if intState == 1 then
			tblFamFilter[IntFilterRecord] = getRecordFilter("FAM")
		end
	end

	local function doRecButtonsReset(strMode)													-- Reset the Delete & Change buttons and Tag Name lists
		-- strMode = StrModeAll if either button is used
		-- strMode = StrModeOld if 'Source Tag Set' is changed
		-- strMode = StrModeNew if 'Target Tag Set' is changed
		btnRecChange.Title = strRecChange
		btnRecChange.Active = "NO"
		if strMode == StrModeOld or strMode == StrModeAll then
			btnRecDelete.Title = strRecDelete
			btnRecDelete.Active = "NO"
			LstRecOldTag.Value = 0
			strRecOldTag = nil
		end
		if strMode == StrModeNew or strMode == StrModeAll then
			LstRecNewTag.Value = 0
			strRecNewTag = nil
		end
	end -- local function doRecButtonsReset

	function LstRecords:action(strText,intItem,iState)										-- Action for Record Type dropdown
		if iState == 1 then
			strRecTag = TblDictionary[strText]													-- Populate Source/Target Tag Set dropdown lists
			doRecButtonsReset(StrModeAll)
			tblRecFilter[IntFilterRecord] = {}													-- Reset the Records filter	-- V2.9
			tglRecRecAll.Value = "ON"
			strRecOldSet,strRecNewSet = StrStrPopulateSetDropList(strRecTag,LstRecOldSet,LstRecNewSet)
			DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strRecOldSet,LstRecOldTag)
			DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strRecNewSet,LstRecNewTag)
		end
	end

	function btnRecDelete:action()																-- Action for Delete tag button
		doChangeData(strRecTag,strRecOldSet,"",strRecOldTag,StrDeleteTag,tblRecFilter)
		doRecButtonsReset(StrModeAll)
	end

	function btnRecChange:action()																-- Action for Change tag button
		doChangeData(strRecTag,strRecOldSet,strRecNewSet,strRecOldTag,strRecNewTag,tblRecFilter)
		doRecButtonsReset(StrModeAll)
	end

	function LstRecOldSet:action(strText,intItem,iState)										-- Action for Source Tag Set dropdown
		if iState == 1 then
			strRecOldSet = strText																	-- Populate Source Tag Name dropdown
			doRecButtonsReset(StrModeOld)
			DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strRecOldSet,LstRecOldTag)
		end
	end

	function LstRecNewSet:action(strText,intItem,iState)										-- Action for Target Tag Set dropdown
		if iState == 1 then
			strRecNewSet = strText																	-- Populate Target Tag Name dropdown
			doRecButtonsReset(StrModeNew)
			DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strRecNewSet,LstRecNewTag)
		end
	end

	function LstRecOldTag:action(strText,intItem,iState)										-- Action for Source Tag Name dropdown
		if iState == 1 then
			strRecOldTag = TblDictionary[strRecTag][strRecOldSet][strText]					-- V2.5
			doUpdateButtons(strRecTag,strRecOldTag,strRecNewTag,btnRecChange,strRecChange,btnRecDelete)
		end
	end

	function LstRecNewTag:action(strText,intItem,iState)										-- Action for Target Tag Name dropdown
		if iState == 1 then
			strRecNewTag = TblDictionary[strRecTag][strRecNewSet][strText]					-- V2.5
			doUpdateButtons(strRecTag,strRecOldTag,strRecNewTag,btnRecChange,strRecChange)
		end
	end

	function txtRecValue:valuechanged_cb()														-- Call back when Filter Value is changed
		tblRecFilter[IntFilterValue] = txtRecValue.Value
	end

	function txtRecDate:valuechanged_cb()														-- Call back when Filter Date is changed
		tblRecFilter[IntFilterDate] = txtRecDate.Value
	end

	function txtRecPlace:valuechanged_cb()														-- Call back when Filter Place is changed
		tblRecFilter[IntFilterPlace] = txtRecPlace.Value
	end

	function tglRecRecAll:action(intState)														-- Action for Record All toggle -- V2.9
		if intState == 1 then
			tblRecFilter[IntFilterRecord] = {}
		end
	end

	function tglRecRecSel:action(intState)														-- Action for Record Pick toggle -- V2.9
		if intState == 1 then
			tblRecFilter[IntFilterRecord] = getRecordFilter(strRecTag)
		end
	end

	function btnViewLog:action()																	-- Action for View & Delete Log Files button
		local tblLogFile = {}																		-- Table of log file names
		tblLogFile[0] = 0																			-- Count of log file names
		for _, arrAttr in ipairs ( general.GetFolderContents(StrProjectPath) ) do
			if arrAttr.path:match(StrProjectPath.."\\"..StrPluginTitle.." - ") then
				tblLogFile[0] = tblLogFile[0] + 1													-- Add log file name to table
				tblLogFile[tblLogFile[0]] = arrAttr.path
			end
		end
		GUI_LogsDialogue(tblLogFile)																-- Display log file popup
	end

	function btnUpdates:action()																	-- Action for Check for Updates button	-- V4.0
		iup_gui.CheckVersionInStore(true)															-- Notify if later Version
	end -- function btnUpdates:action

	function btnDefault:action()																	-- Action for Restore Defaults button
		ResetDefaultSettings()
		setControls()
		iup_gui.ShowDialogue("Main")
		iup_gui.DefaultDialogue()																	-- V4.0
		SaveSettings()																				-- Save sticky data settings
	end -- function btnDefault:action

	function btnSetFont:action()																	-- Action for User Interface Font button
		btnSetFont.Active = "NO"
		iup_gui.FontDialogue(tblControls)
		SaveSettings()																				-- Save sticky data settings
		btnSetFont.Active = "YES"
	end -- function btnSetFont:action

	function btnSetFont:button_cb(intButton,intPress)											-- Action for mouse right-click on Set Window Fonts button
		if intButton == iup.BUTTON3 and intPress == 0 then
			iup_gui.BalloonToggle()																	-- Toggle tooltips Balloon mode
		end
	end -- function btnSetFont:button_cb

	local function doExecute(strExecutable, strParameter)										-- Invoke FH Shell Execute API	-- V3.1
		local function ReportError(strMessage)
			iup_gui.WarnDialogue(	"Shell Execute Error",
									"ERROR: "..strMessage.." :\n"..strExecutable.."\n"..strParameter.."\n\n",
									"OK"	)
		end -- local function ReportError
		return general.DoExecute(strExecutable, strParameter, ReportError)
	end -- local function doExecute

	local strHelp = "https://pluginstore.family-historian.co.uk/page/help/change-any-fact-tag"
	local arrHelp = { "-individual-records-tab"; "-family-records-tab"; "-other-records-tab"; "-status-and-settings-tab"; }

	function btnGetHelp:action()																	-- Action for Help and Advice button	-- V3.1
		local strPage = arrHelp[intTabPosn] or ""
		doExecute( strHelp..strPage )
		fhSleep(3000,500)
		dialogMain.BringFront="YES"
	end -- function btnGetHelp:action

	iup_gui.ShowDialogue("Main",dialogMain,btnClose,"map")									-- Map the Main dialogue for dropdown lists, etc

	iup_gui.AssignAttributes(tblControls)														-- Assign GUI control attributes

	strRecTag = StrPopulateDropdownLists("INDI")												-- Populate the Individual tab dropdown lists		-- 17 Aug 2013

	if #ArrLogFileText > 0 then LblLogFile.Title = StrLogFilePath end						-- Update the log filename display	-- V3.7

	progbar.Setup(iup_gui.DialogueAttributes("Bars"))											-- Setup the progress bar attributes

	iup_gui.ShowDialogue("Main")																	-- Show the Main dialogue

	if flgQuitMode then
		SaveSettings()																				-- Save sticky data settings
		if #TblPtrName > 0 then																		-- 	If no changes then just Close else message to use Undo Plugin Updates	-- V3.7
			fhMessageBox("\n To reverse all changes please use the command \n\n                Edit > Undo Plugin Updates \n","MB_OK","MB_ICONINFORMATION")
		end
	end

end -- function GUI_MainDialogue

-- Main body of Plugin script starts here --

	local intPause = collectgarbage("setpause",50)												-- Default = 200  Aggressive = 50	-- Sets pause of collector and returns prev value of pause	-- V3.4

	local intStep = collectgarbage("setstepmul",300)											-- Default = 200  Aggressive = 300	-- Sets step mult of collector & returns prev value of step	-- V3.4

	fhInitialise(5,0,8,"save_recommended")														-- V5.0.2 fhCreateItem FILE/FORM update, V5.0.8 for sticky settings with scope

	PresetGlobalData()																				-- Preset global data definitions

	ResetDefaultSettings()																			-- Preset default sticky settings

	LoadSettings()																					-- Load sticky data settings

	LoadFactsAndTags()																				-- Load all defined Fact Sets, undefined Custom Facts, Gedcom Defined Tags, and UDF Tag Sets 

	GUI_MainDialogue()																				-- Invoke main dialogue

	SaveSettings()																					-- Save sticky data settings

	if #TblPtrName > 0 then																			-- Create Query Result Set
		fhOutputResultSetTitles("Results Log")
		fhOutputResultSetColumn("Record Name",	"item", TblPtrName, #TblPtrName, 120, "align_left")
		fhOutputResultSetColumn("Old Item"	,	"text", TblOldItem, #TblPtrName, 060, "align_left")
		fhOutputResultSetColumn("Old Data"	,	"text", TblOldData, #TblPtrName, 200, "align_left")
		fhOutputResultSetColumn("Action"		,	"text", TblPerform, #TblPtrName, 030, "align_left")
		fhOutputResultSetColumn("New Item"	,	"text", TblNewItem, #TblPtrName, 060, "align_left")
		fhOutputResultSetColumn("Link Data"	,	"item", TblPtrData, #TblPtrName, 200, "align_left")
	end

Source: Change-Any-Fact-Tag-13.fh_lua