Plugins and Language Packs for Family Historian

Backup and Restore Family Historian Settings.fh_lua

--[[
@Title:			Backup and Restore Family Historian Settings
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			3.8
@Keywords:		
@LastUpdated:		05 Oct 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:		Backup and Restore the Family Historian Custom & Preference Settings using any folder.
@Version Log:		See end of file.
]]

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

function loadrequire(module,extended)
	if not(extended) then extended = module end
	local function installmodule(module,filename)
		local bmodule = false
		if not(filename) then
			filename = module..'.mod'
			bmodule = true
		end
		local storein = fhGetContextInfo('CI_APP_DATA_FOLDER')..'\\Plugins\\'
		-- Check if subdirectory needed
		local path = string.match(filename, "(.-)[^/]-[^%.]+$")
		if path ~= "" then
			path = path:gsub('/','\\')
			-- Create sub-directory
			lfs.mkdir(storein..path)
		end
		-- Get file down and install it
		local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
		local url = "http://www.family-historian.co.uk/lnk/getpluginmodule.php?file="..filename
		http:Open("GET",url,false)
		http:Send()
		http:WaitForResponse(30)
		local status = http.StatusText
		if status == 'OK' then
			length = http:GetResponseHeader('Content-Length')
			data = http.ResponseBody
			if bmodule then
				local modlist = loadstring(http.ResponseBody)
				for _,f in pairs(modlist()) do
					if not(installmodule(module,f)) then
						break
					end
				end
			else
				local function OpenFile(strFileName,strMode)
					local fileHandle, strError = io.open(strFileName,strMode)
					if not fileHandle then
						error("\n Unable to open file in \""..strMode.."\" mode. \n "..
						strFileName.." \n "..tostring(strError).." \n")
					end
					return fileHandle
				end -- OpenFile
				local function SaveStringToFile(strString,strFileName)
					local fileHandle = OpenFile(strFileName,"wb")
					fileHandle:write(strString)
					assert(fileHandle:close())
				end -- SaveStringToFile
				SaveStringToFile(data,storein..filename)
			end
			return true
		else
			fhMessageBox('An error occurred in Download please try later',"MB_OK","MB_ICONEXCLAMATION")
			return false
		end
	end
	local function requiref(module)
		require(module)
	end
	res = pcall(requiref,extended)
	if not(res) then
		local ans = fhMessageBox(
		'This plugin requires '..module..' support, please click OK to download and install the module',
		'MB_OKCANCEL','MB_ICONEXCLAMATION')
		if ans ~= 'OK' then
			return false
		end
		if installmodule(module) then
			package.loaded[extended] = nil -- Reset Failed Load
			require (extended)
		else
			return false
		end
	end
	return true
end -- function loadrequire

-- Global Data Definitions --
function PresetGlobalData()

	if fhGetAppVersion() > 6 then																	-- Load md5 library for handling very long file paths	-- V3.1
		general.DetectOldModules()																	-- V3.2
		general.DeleteFile(fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugins\\md5.lua")-- V3.2
		md5 = require("md5")
	else 
		if not(loadrequire("md5")) then return end
	end

	iup_gui.SetUtf8Mode()																			-- V3.2
	iup_gui.Margin = "4x4"

	local strRegKey = iup_gui.GetRegKey("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\PROCESSOR_ARCHITECTURE")
	StrProcArch64 = not ( strRegKey == "x86" )													-- 64-bit or 32-bit PC for WOW6432Node support	-- V3.2 -- V3.5
																										-- Filename Global Constants
	StrPluginRoot = iup_gui.ProjectRoot															-- Project Plugin data file root name
	StrPublicPath = iup_gui.PublicPath															-- Public data folder path name
	StrCalicoPie  = iup_gui.CalicoPie																-- Program Data Calico Pie path name
	StrApplicPie  = os.getenv("APPDATA").."\\Calico Pie"										-- Application Data Calico Pie root to Plugin Data & Preferences	-- 11 Feb 2014
	StrPublicPie  = os.getenv("PUBLIC") or os.getenv("ALLUSERSPROFILE")					-- Windows 8/7/Vista or Windows XP Public folder
	StrPublicPie  = StrPublicPie.."\\Documents\\Calico Pie"									-- Public Documents Calico Pie root to Tutorial Files
	StrProg_Data  = "\\Program Data"																-- Backup data Program Data subfolder
	StrPlug_Data  = "\\Plugin Data"																-- Legacy data Plugin Data subfolder	-- V2.4
	StrUser_Data  = "\\Application"																-- Backup data Application subfolder	-- V2.4
	StrTutorials  = "\\Tutorial Files"															-- Backup data Tutorial Files subfolder
	StrTutorFiles = "Tutorial Files"																-- Dialogue buttons
	StrRegDatKeys = "Registry Data Keys"
	StrLogFile = iup_gui.MachineRoot..".log"													-- ICACLS and WHOAMI /GROUPS log file	-- V2.9
	StrBatFile = iup_gui.MachineRoot..".bat"													-- Command prompt batch file
	StrVbsFile = iup_gui.MachineRoot..".vbs"													-- Visual Basic Script file
	StrRegFile = iup_gui.MachineRoot..".reg"													-- Registry keys output file
	StrRegName = StrRegFile:gsub(StrCalicoPie.."\\","")
	general.DeleteFile(StrLogFile)
	general.DeleteFile(StrBatFile)
	general.DeleteFile(StrRegFile)

	StrBackupDir = ""																				-- Default Backup & Restore folder path
	IntBackupMod = 0																					-- Backup Modified Date-Time

	StrBackupKey = ""																				-- FH Preferences Backup folder path

	TblProgram = {}																					-- Family Historian Programs & Versions
	TblVersion = {}
	IsTutorial = true																				-- Tutorial backup files? -- V2.4

end -- function PresetGlobalData

-- Reset Sticky Settings to Default Values --
function ResetDefaultSettings()
	iup_gui.CustomDialogue("Main","0x0")															-- GUI window minimum size and central position for "Main"
	iup_gui.DefaultDialogue("Mode")																-- GUI window RasterSize and X & Y co-ordinates for "Main","Font" & "Mode" dialogues
	StrRegistry = "OFF"																				-- Allow RESTORE Registry Data: OFF
	StrDiagnose = "OFF"																				-- Enable Diagnostic Mode: OFF
	StrDatedDir = "OFF"																				-- Use Dated Sub-folders: OFF -- 19 July 2013
	StrLaterVer = "OFF"																				-- Later Version allowed: OFF -- V2.5
end -- function ResetDefaultSettings

-- Load Sticky Settings from File --
function LoadSettings()
	iup_gui.LoadSettings()																			-- Includes "Main","Font" dialogues and "FontSet" & "History"
	iup_gui.Balloon = "NO"																			-- V2.9 for Crossover/PlayOnLinux/Mac
	StrBackupDir = tostring(iup_gui.LoadFolder("Backup", StrBackupDir))
	StrRegistry  = tostring(iup_gui.LoadGlobal("Registry",StrRegistry))
	StrDiagnose  = tostring(iup_gui.LoadGlobal("Diagnose",StrDiagnose))
	StrDatedDir  = tostring(iup_gui.LoadGlobal("DatedDir",StrDatedDir))					-- 19 July 2013
	SaveSettings()																					-- Save sticky data settings
end -- function LoadSettings

-- Save Sticky Settings to File --
function SaveSettings()
	iup_gui.SaveFolder("Backup", StrBackupDir)
	iup_gui.SaveGlobal("Registry",StrRegistry)
	iup_gui.SaveGlobal("Diagnose",StrDiagnose)
	iup_gui.SaveGlobal("DatedDir",StrDatedDir)													-- 19 July 2013
	iup_gui.SaveSettings()																			-- Includes "Main","Font" dialogues and "FontSet" & "History"
end -- function SaveSettings

-- Check Folder Write Access and try to keep Date Modified --								-- V2.9 -- lfs permissions attribute is not effective
function FlgFolderWrite(strFolder)
	if general.FlgFolderExists(strFolder) then
		local strLog = StrLogFile																	-- Log file for results
		local strDir = strFolder																	-- Directory to check
		local luaShell = luacom.CreateObject("WScript.Shell")									-- Run CMD shell for UserDomain\UserName & ICACLS & WHOAMI /GROUPS in hidden window
		luaShell:Run('CMD /C ECHO %UserDomain%\\%UserName% >> "'..strLog..'" & ICACLS "'..strDir..'" /C /L /Q >> "'..strLog..'" & WHOAMI /GROUPS >> "'..strLog..'"', 0, true)
		local strAcls = ""																			-- ICACLS report text
		local strUser = ""																			-- Current account Username
		local strGrps = ""																			-- Current account Groups
		if general.FlgFileExists(strLog) then
			local strFile = general.StrLoadFromFile(strLog)									-- Load log and split into Username, ICACLS report, WHOAMI /GROUPS list
			general.DeleteFile(strLog)
			strUser, strAcls, strGrps = strFile:match("^(.-)([\r\n]+.-Successfully processed %d files; Failed processing %d files[\r\n]+).+========([\r\n]+.+)$")	-- V3.2 [\r\n]+
			strUser = strUser or ""
			strUser = strUser:gsub(" +$","")														-- UserDomain\UserName from first line
			strAcls = strAcls or ""
			strAcls = strAcls:gsub(" +"," ")														-- Tidy ICACLS report
			strAcls = strAcls:gsub("(No permissions.+users have full control)",strUser..":(F)[\r\n]+ %1")	-- V3.2 [\r\n]+
			strGrps = strGrps or ""
			strGrps = "\n"..strUser.."   "..strGrps												-- Include User name with Group names
		end
		for strName in strGrps:gmatch("[\r\n]+(%u.-)   ") do									-- V3.2 [\r\n]+
			local strHide = strName:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])","%%%1") 			-- Hide magic pattern symbols in User/Group name
			for strPerm in strAcls:gmatch(" "..strHide..":.-%(([%u,]+)%)[\r\n]+") do		-- Extract matching ACL permissions in trailing parentheses	-- V3.2 [\r\n]+
				if strPerm and strPerm:match("[FMW]") then return true end					-- Match Full, Modify, Write access
			end
		end
	end
	return general.FlgFolderWrite(strFolder)													-- If that fails use write & delete file method that alters Date Modified
end -- function FlgFolderWrite

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

-- GUI Mode Prompt Dialogue --
function GUI_ModeDialogue(strHead,strText,...)
	local arg = {...}
	local strAnswer = ""																			-- Name of Button pressed
	local btnAnswer = iup.button{}
	local iupHbox = iup.hbox{ Margin=iup_gui.Margin; Homogeneous="NO"; Expand="YES"; }
	for intButton, strButton in ipairs( arg ) do												-- Set or hide each GUI Button according to Button arguments
		local strVisible = "YES"
		local strActive = "YES"
		local strColor = iup_gui.Safe
		if strButton == "" then strVisible = "NO" strActive = "NO" end
		if strButton:match(StrTutorFiles) and not IsTutorial then strVisible = "NO" strActive = "NO" end		-- V2.4 IsTutorial
		if strButton:match(StrRegDatKeys) and StrRegistry == "OFF" then strActive = "NO" end
		if strButton:match("Skip") or strButton:match("Cancel") then strColor = iup_gui.Risk end
		local btnButton = iup.button{ Title=strButton; Expand="YES"; Size="x10"; Visible=strVisible; Active=strActive; FgColor=strColor; action=function() strAnswer=strButton return iup.CLOSE end; }
		iup.Append(iupHbox,btnButton)
		if intButton == #arg then
			btnAnswer = btnButton																	-- Right-hand button gets focus and sets Default/Close strAnswer
			strAnswer = strButton
		end
	end
	for intArg = 1, 4 - #arg do																	-- Pad with up to 4 hidden buttons to maintain a tidy layout
		local btnButton = iup.button{ Title=""; Expand="YES"; Size="x10"; Visible="NO"; Active="NO"; }
		iup.Insert(iupHbox,nil,btnButton)
	end
	local strColor = iup_gui.Safe																	-- Set text message colour appropriately
	if strText:match("WARNING: ") then strColor = iup_gui.Warn end
	if strText:match("ERROR: ")   then strColor = iup_gui.Risk end
	-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
	local dialogMode = iup.dialog { Title=iup_gui.Plugin.."  "..strHead; TopMost="YES"; -- was BringFront="YES"; -- V2.0
								iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Margin;
									iup.frame { Font=iup_gui.FontHead; FgColor=strColor; Title=strHead;
										iup.vbox { Font=iup_gui.FontBody; FgColor=strColor;
											iup.label { Title=strText; Tip="Operational message";	TipBalloon=iup_gui.Balloon; WordWrap="YES"; Expand="YES"; };
											iupHbox;
										};
									};
								};
							}
	local tblAttr = iup_gui.DialogueAttributes("Main")										-- Mode GUI is Main GUI width and near bottom of Main GUI
	iup_gui.CustomDialogue( "Mode", tostring(tblAttr.Width), tblAttr.CoordX, tblAttr.CoordY + tblAttr.Height - 40 )
	iup_gui.ShowDialogue( "Mode", dialogMode, btnAnswer, "popup" )
	return strAnswer
end -- function GUI_ModeDialogue

-- GUI Main Dialogue --
function GUI_MainDialogue()

	local strButtonTitle = " all Family Historian Custom and Preference Settings "
	local strRelaxFhVers = "Allow the FH Program Version to be later than the Backup Folder Version"	-- V2.6 -- V3.0

	-- Create the Main Dialogue Common controls with Title/Value, etc
	local btnBackup		= iup.button	{ Title="  BACKUP"..strButtonTitle; }
	local btnRestore		= iup.button	{ Title=" RESTORE"..strButtonTitle; }
	local lblBackup		= iup.label	{ Title="This lets you Backup your Family Historian Custom && Preference settings to a backup folder.\nRun it on old PC before moving to new PC, and get help from FHUG on how to use the data."		; WordWrap="YES"; }
	local lblRestore		= iup.label	{ Title="This mode Restores the Family Historian Custom && Preference settings from a backup folder.\nAre you fully aware of the risks of a Restore?  Have you a Backup of the current settings data?"	; WordWrap="YES"; }
	local lblOptions		= iup.label	{ Title="This mode allows various optional settings to be adjusted."; WordWrap="YES"; }
	local btnFolder		= iup.button	{ Title="Choose Backup Folder"; }
	local btnExplore		= iup.button	{ Title="Open Backup Folder"; }						-- V2.6
	local btnDefault		= iup.button	{ Title="Reset Plugin Defaults"; }					-- V3.0
	local btnSetFont		= iup.button	{ Title="Set Window Fonts"; }
	local tglDatedDir	= iup.toggle	{ Title=" Utilise Dated Sub-folders"; }				-- 19 July 2013
	local tglDiagnose	= iup.toggle	{ Title=" Enable Diagnostic Mode"; }
	local tglRegistry	= iup.toggle	{ Title=" Allow Restore Registry Data"; }
	local tglLaterVer	= iup.toggle	{ Title=" "..strRelaxFhVers.." ~ Use at your own risk!"; }	-- V2.5 -- V2.6
	local labProgram		= iup.label	{ Title="FH Program:"		; Size="60"; }
	local lblProgram		= iup.label	{ Title=" "; }
	local labBackData	= iup.label	{ Title="Backup Folder:"	; Size="60"; }			-- V3.0
	local lblBackData	= iup.label	{ Title=" "; }
	local labProgData	= iup.label	{ Title="Settings Path:"	; Size="60"; }			-- V3.0
	local lblProgData	= iup.label	{ Title=" "; }
	local labFilename	= iup.label	{ Title="Settings File:"	; Size="60"; }			-- V3.0
	local lblFilename	= iup.label	{ Title=" "; }
	local labStatus		= iup.label	{ Title="Status:"			; Size="60"; }
	local lblStatus		= iup.label	{ Title=" "; }
	local btnUpdates		= iup.button	{ Title="Check for Updates"; }						-- V3.8
	local btnGetHelp		= iup.button	{ Title=" Help && Advice "; }
	local btnDestroy		= iup.button	{ Title=" Close Plugin "; }

	-- Create the Backup box
	local vboxBackup		=	iup.vbox { Gap=iup_gui.Gap; lblBackup;
									iup.hbox { iup.fill{}; btnBackup; iup.fill{}; };
								}

	-- Create the Restore box
	local vboxRestore	=	iup.vbox { Gap=iup_gui.Gap; lblRestore;
									iup.hbox { iup.fill{}; btnRestore; iup.fill{}; };
								}

	-- Create the Options box
	local vboxOptions	=	iup.vbox { Gap=iup_gui.Gap; lblOptions;
									iup.hbox { btnFolder; btnExplore; btnDefault; btnSetFont; Homogeneous="YES"; };
									iup.hbox { tglDatedDir; tglDiagnose; tglRegistry; Homogeneous="YES"; };
									iup.hbox { tglLaterVer; Homogeneous="YES"; };				-- V2.5
								}

	-- Create the Tab controls
	local tabControls	=	iup.tabs {															-- Padding="8x4" moved to setControls()	-- V3.0
									vboxBackup ; tabtitle0=" Backup  ";
									vboxRestore; tabtitle1=" Restore ";
									vboxOptions; tabtitle2=" Options ";
								}

	-- Create the Status frame
	local vboxStatus		=	iup.vbox { Gap=iup_gui.Gap;
									iup.hbox { labProgram ; lblProgram ; };
									iup.hbox { labBackData; lblBackData; };
									iup.hbox { labProgData; lblProgData; };
									iup.hbox { labFilename; lblFilename; };
									iup.hbox { labStatus  ; lblStatus  ; };
									iup.hbox { btnUpdates; btnGetHelp; btnDestroy; Homogeneous="YES"; };	-- V3.8
								}
	local frameStatus	= 	iup.frame { Title=" Status "; vboxStatus; }

	-- Create the Main dialogue
	local vboxMain		=	iup.vbox { Margin=iup_gui.Margin;
									tabControls;
									frameStatus;
								}
	local dialogMain		=	iup.dialog { Title=iup_gui.Plugin..iup_gui.Version;
									vboxMain;
								}

	local function setControls()																	-- Set GUI control values
		if fhGetAppVersion() > 6 then																-- FH V7 IUP 3.28	-- V3.0
			tabControls.TabPadding = "8x4"
		else																							-- FH V6 IUP 3.11	-- V3.0
			tabControls.Padding = "8x4"
		end
		tglDatedDir.Value = StrDatedDir															-- 19 July 2013
		tglDiagnose.Value = StrDiagnose
		tglRegistry.Value = StrRegistry
		lblProgram .Title = "Family Historian ..."
		lblBackData.Title = StrBackupDir
		lblProgData.Title = StrCalicoPie
		lblFilename.Title = " "
		lblStatus  .Title = " "
	end -- local function setControls

	local strToolTip = " all Program Data, Tutorial, and Registry custom settings, preferences, etc..."
	local strRight = "ARIGHT:ABOTTOM"

	-- Set other GUI control attributes
	local tblControls={ { "Font"; "FgColor"; "Alignment"; "Expand"; "Tip"; {"TipBalloon";"Balloon"}; setControls; };
		[vboxMain  ]  = { "FontHead"; "Head"; "ACENTER"; };
		[frameStatus] = { "FontHead"; "Head"; false;     };
		[tabControls] = { "FontHead"; "Head"; false;      false; "Choose between Backup, Restore, and Options"; };
		[btnBackup ]  = { "FontHead"; "Safe"; false; "VERTICAL"; "Backup" ..strToolTip; };
		[btnRestore]  = { "FontHead"; "Risk"; false; "VERTICAL"; "Restore"..strToolTip; };
		[lblBackup ]  = { "FontBody"; "Safe"; "ACENTER";  "YES"; "Backup" ..strToolTip; };
		[lblRestore]  = { "FontBody"; "Risk"; "ACENTER";  "YES"; "Restore"..strToolTip; };
		[lblOptions]  = { "FontBody"; "Body"; "ACENTER";  "YES"; "This mode allows various optional settings to be adjusted."; };
		[btnFolder ]  = { "FontBody"; "Safe"; false    ;  "YES"; "Choose backup data folder path"; };
		[btnExplore]  = { "FontBody"; "Safe"; false    ;  "YES"; "Open backup data folder in Windows File Explorer"; };	-- V2.6
		[btnDefault]  = { "FontBody"; "Safe"; false    ;  "YES"; "Reset to default Plugin Settings and Window positions and sizes"; };
		[btnSetFont]  = { "FontBody"; "Safe"; false    ;  "YES"; "Choose user interface window font styles"; };
		[tglDatedDir] = { "FontBody"; "Safe"; false    ;  "YES"; "Utilise date/time stamped sub-folders for backups"; }; -- 19 July 2013
		[tglDiagnose] = { "FontBody"; "Safe"; false    ;  "YES"; "Enable a Diagnostic mode to assist with debugging"; };
		[tglRegistry] = { "FontBody"; "Risk"; false    ;  "YES"; "Allow RESTORE of Windows Registry Data Keys settings"; };
		[tglLaterVer] = { "FontBody"; "Risk"; false    ;  "YES"; "Relax the FH Program Version checks to allow earlier Backup Version to be Restored"; };	-- V2.5
		[labProgram ] = { "FontBody"; "Body"; strRight ;  false; "Family Historian program version"; };
		[lblProgram ] = { "FontBody"; "Safe"; "ALEFT"  ;  "YES"; "Family Historian program version"; };
		[labBackData] = { "FontBody"; "Body"; strRight ;  false; "Backup data folder path"; };
		[lblBackData] = { "FontBody"; "Safe"; "ALEFT"  ;  "YES"; "Backup data folder path"; };
		[labProgData] = { "FontBody"; "Body"; strRight ;  false; "Program data folder path"; };
		[lblProgData] = { "FontBody"; "Safe"; "ALEFT"  ;  "YES"; "Program data folder path"; };
		[labFilename] = { "FontBody"; "Body"; strRight ;  false; "Current data filename"; };
		[lblFilename] = { "FontBody"; "Safe"; "ALEFT"  ;  "YES"; "Current data filename"; };
		[labStatus ]  = { "FontBody"; "Body"; strRight ;  false; "Status and progress messages"; };
		[lblStatus ]  = { "FontBody"; "Safe"; "ALEFT"  ;  "YES"; "Status and progress messages"; };
		[btnUpdates]  = { "FontBody"; "Safe"; false    ;  "YES"; "Check for a later plugin version in the Plugin Store"; };	-- V3.8
		[btnGetHelp]  = { "FontBody"; "Safe"; false    ;  "YES"; "Obtain online Help and Advice from the Plugin Store"; };
		[btnDestroy]  = { "FontBody"; "Risk"; false    ;  "YES"; "Close the Plugin"; };
		}

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

	-- Backup and Restore Data Functions

	function doNothing()																			-- Used to inhibit general library error messages
	end -- function doNothing

	function doMakeFolder(strFolder,strHeader)													-- Make a subfolder and report any errors
		if not general.MakeFolder(strFolder,doNothing) then
			GUI_ModeDialogue(	"Cannot Make Folder",
									"ERROR: Cannot make "..strHeader..".\n"..strFolder.."\nOperation cancelled.\n",
									"OK"	)
			return false
		end
		return true
	end -- function doMakeFolder

	local dicDataMode =
	{
		Backup  = { Source="Settings Path"; Target="Backup Folder"; } ;						-- V3.2 { Source="Program Data"; Target="Backup Data"; }
		Restore = { Source="Backup Folder"; Target="Settings Path"; } ;						-- V3.2 { Source="Backup Data"; Target="Program Data"; }
		Migrate = { Source="Settings Path"; Target="Backup Folder"; } ;						-- V3.2 { Source="Program Data"; Target="Backup Data"; }
	}
	local dicMd5ToPath = {}																			-- Dictionary of md5 filenames versus long path names	-- V3.1
	local dicPathToMd5 = {}																			-- Dictionary of long path names versus md5 filenames	-- V3.1

	local function intTime(strFilePath,intDefault)												-- Get file modified Unix epoch date-time or use default	-- V3.4 - V3.5
		local intTime = intDefault
		local strTime = "?"
		local oldAbort_on_Error = luacom.config.abort_on_error
		luacom.config.abort_on_error = false														-- luacom error protection	-- V3.5
		luacom.config.last_error = nil
		if general.FlgFileExists(strFilePath) then
			local intMax = 3																			-- Repeat until modified date-time is rational	-- V3.5
			repeat
				local fileObject = general.FSO:GetFile(strFilePath)
				luacom.DateFormat = "table"
				local dicMod = fileObject.DateLastModified or {}								-- Get table of standard date-time fields -- V3.4 -- V3.5
				if (dicMod.Year or 0) >= 1970 then
					intTime = os.time({ year=dicMod.Year or 1970; month=dicMod.Month or 1; day=dicMod.Day or 1; hour=dicMod.Hour or 0; min=dicMod.Minute or 0; sec=dicMod.Second or 0; })
				end
				luacom.DateFormat = "string"
				strTime = fileObject.DateLastModified											-- Get string date-time for current locale settings -- V3.4
				intMax = intMax - 1
			until intTime > 0 or intMax < 0
		end
		if luacom.config.last_error then															-- V3.5
			fhMessageBox("\n intTime function failed. \n")
		end
		luacom.config.abort_on_error = oldAbort_on_Error
		return intTime, strTime
	end -- local function intTime

	local function intSize(strFilePath,intDefault)												-- Get byte size of file or use default	-- V3.2
		local intSize = intDefault
		if general.FlgFileExists(strFilePath) then
			local fileObject = general.FSO:GetFile(strFilePath)
			intSize = fileObject.Size
		end
		return intSize
	end -- local function intSize

	local function intSizeTime(strFilePath,intDefault)										-- Get byte size and modified date-time of file	-- V3.2 -- V3.4
		return intSize(strFilePath,intDefault), intTime(strFilePath,intDefault)
	end -- local function intSizeTime

	local function newCopyFile(strSourceFile,strTargetFile,strFunction,strFileName)		-- Optionally copy a new File and set its Date-Time-Stamps	-- V3.2
		local intSourceSize,intSourceTime,strSourceTime = intSizeTime(strSourceFile,0)	-- Get the Source file byte size and modified date-time	-- V3.2 -- V3.4
		local intTargetSize,intTargetTime,strTargetTime = intSizeTime(strTargetFile,1)	-- Get the Target file byte size and modified date-time	-- V3.2 -- V3.4
		local intDifferSize = math.abs( intSourceSize - intTargetSize )						-- Difference between Source and Target sizes & date-times
		local intDifferTime = math.abs( intSourceTime - intTargetTime )
		local intDifferHour = math.abs( intDifferTime - 3600 ) 
		if intDifferSize == 0 and intDifferHour <= 2											-- Same byte sizes but 1-hour (±2 sec) different date-times
		and intSourceTime < IntBackupMod and intTargetTime < IntBackupMod then			-- and both pre-date the Backup Data modified date-time
			intDifferTime = 0																		-- So ignore Daylight Saving Time (DST/GMT/BST) 1-hour offset
		end
		if intDifferSize > 0 or intDifferTime > 2												-- Only need to copy file with different size or date-time > ±2 sec
		and not strSourceFile:match("Backup and Restore Family Historian Settings%.[bd]at$")			-- V3.2
		and not strTargetFile:match("Backup and Restore Family Historian Settings%.[bd]at$") then		-- V3.2
			local strButton = "Copy"
			if intSourceTime < intTargetTime and strFunction ~= "Migrate" then
				if StrButton then
					strButton = StrButton
				else
					lblFilename.Title = strFileName:gsub("&","&&")								-- Ensure GUI shows current file	-- V3.2
					local strSourceData = dicDataMode[strFunction].Source						-- Adjust dialogue for Backup or Restore function -- V2.4
					local strTargetData = dicDataMode[strFunction].Target
					strButton = GUI_ModeDialogue(	strFunction.." Old File : "..strFileName,	-- V3.2 -- V3.4
															"WARNING: "..strSourceData.." File modified "..strSourceTime.."\nis older than "..strTargetData.." File modified "..strTargetTime..".\nEither copy old "..strSourceData.." File over newer "..strTargetData.." File, or keep the newer "..strTargetData.." File?",
															"Copy Old "..strSourceData.." File", "Keep New "..strTargetData.." File", "Copy All Old Files", "Keep All New Files"	)
					if strButton:match("All %u%l%l Files") then
						StrButton = strButton														-- Remember Copy/Keep All Files option 
					end
				end
			end
			if strButton:match("Copy") then
				general.FSO:CopyFile(strSourceFile,strTargetFile)								-- Copy file using FSO including access & modified date-times	-- V3.1	-- V3.2
	--			print("Source later than Target or wants copy. Date: "..strSourceTime.." Size: "..intSourceSize.." File: "..strSourceFile)	-- Diagnostic
			else
	--			print("Source older than Target so not copied. Date: "..strSourceTime.." Size: "..intSourceSize.." File: "..strSourceFile)	-- Diagnostic
			end
		else
	--			print("Source same as Target so is not copied. Date: "..strSourceTime.." Size: "..intSourceSize.." File: "..strSourceFile)	-- Diagnostic
		end
	end -- local function newCopyFile

	local function doCopyFile(strSourceFile,strTargetFile,strFunction,strFileName)		-- Copy each valid Source file to Target file in protected mode	-- V3.1 -- V3.2
		local isOK, strErr = pcall(newCopyFile,strSourceFile,strTargetFile,strFunction,strFileName)
		if not isOK then
			local intSize = intSize(strSourceFile,0)											-- Get the Source file byte size and report error	-- V3.2
			fhMessageBox(tostring(strErr).."\n\n"..strFunction.."     File Size="..tostring(intSize).." Bytes     Memory="..math.floor(collectgarbage("count")+0.5).." KB\n\n"..strSourceFile.."\n\n"..strTargetFile,"MB_OK","MB_ICONEXCLAMATION")
		end
	end -- local function doCopyFile

	local function doFileStatus(intCount,strTitle,strFileName)								-- Show data file count and prevent 'Not responding'	-- V3.2
		if intCount % 23 == 0 or intCount < 20 then
			lblStatus.Title = strTitle..intCount.." data files..."
			lblFilename.Title = strFileName:gsub("&","&&")
			if intCount % 115 == 0 then															-- Every 5th time
				fhSleep(9,5)
				collectgarbage("collect")
			end
		end
	end -- local function doFileStatus

	local function intCopyFiles(strSourceDir,strTargetDir,strProgName,strFunction,intMinimum)	-- Copy all Files from Source to Target folder for chosen Program
		local intFileCount = 0
		local tblFilePaths = general.GetFolderContents(strSourceDir)						-- V3.6
		for _,tblAttr in ipairs (tblFilePaths) do												-- V3.6
			local strPath = tblAttr.path
			if tblAttr.mode == "directory" and not strPath:match("\\Map$") then 			-- Exclude large \Map... directory	-- V3.6
				for _,tblAttr in ipairs (general.GetFolderContents(strPath,true)) do
					if tblAttr.mode == "file" then
						intFileCount = intFileCount + 1											-- Count the Source files to be copied
						if intFileCount > intMinimum then break end
					end
				end
			end
			if intFileCount > intMinimum then break end
		end
		if intFileCount < intMinimum then
			lblFilename.Title = " "
			GUI_ModeDialogue(	"Too Few Data Files",
									"ERROR: "..strFunction.." NOT performed for this subset of files, as only "..intFileCount.." data files found in:\n"..strSourceDir.."\n",
									"OK"	)
			return 0
		end
		if intFileCount == 0 then																	-- Escape if no files to copy	-- V3.2
			return intFileCount
		end
		if not doMakeFolder(strTargetDir,strFunction.." "..strProgName.." folder. ") then return -1 end -- Ensure root folder exists
		intFileCount = 0
		lblStatus.Title = " "
		StrButton = nil																				-- Used to remember Copy/Keep All Files option within doCopyFile
		for _,tblAttr in ipairs (tblFilePaths) do												-- V3.6
			if tblAttr.mode == "directory" then													-- V3.8
				local strPath = tblAttr.path
				local strDir = strPath:replace(strSourceDir,strTargetDir)					-- Ensure a root directory exists	-- V3.6
				if not doMakeFolder(strDir,strFunction.." "..strProgName.." folder. ") then return -1 end
				for _,tblAttr in ipairs (general.GetFolderContents(strPath,true)) do		-- V3.6
					local strSourceFile = tblAttr.path
					local strTargetFile = strSourceFile:replace(strSourceDir,strTargetDir)
					local strFileName   = strSourceFile:replace(strSourceDir.."\\","")
					if not strSourceFile:match("\\Map\\Cache") then							-- Exclude large \Map\Cache\... directory	-- V3.8
						if strFunction:match("Backup") then
							if #strTargetFile > 250 then											-- Backup long path names exceeding 250 chars	-- V3.1
								if tblAttr.mode == "directory" then
									strTargetFile = strTargetDir
								else
									local md5FileName = md5.sumhexa(strFileName)				-- Convert long path to md5 filename and save	-- V3.1
									dicMd5ToPath[md5FileName] = strFileName
									strTargetFile = strTargetDir.."\\"..md5FileName
								end
							elseif next(dicMd5ToPath) then
								local md5FileName = dicPathToMd5[strFileName]
								if md5FileName then
									general.DeleteFile(strTargetDir.."\\"..md5FileName)		-- Delete any redundant short md5 filename		-- V3.1
									dicMd5ToPath[md5FileName] = nil
									dicPathToMd5[strFileName] = nil
								end
							end
						elseif strFunction:match("Restore") and tblAttr.mode == "file"
						and next(dicMd5ToPath) and dicMd5ToPath[strFileName] then 			-- Restore file with long path names	-- V3.1 -- V3.2
							strFileName = dicMd5ToPath[strFileName] or strFileName
							strTargetFile = strTargetDir.."\\"..strFileName					-- Convert any md5 filename to long path name	-- V3.1
							local strPath = general.SplitFilename(strTargetFile)
							if not doMakeFolder(strPath,strFunction.." "..strProgName.." folder. ") then return -1 end
						end
						if tblAttr.mode == "directory" then										-- Must create any missing folders in Target folder
							if not doMakeFolder(strTargetFile,strFunction.." "..strProgName.." folder. ") then return -1 end
						else
							if tblAttr.mode == "file" then
								if not strSourceFile:match("~fh.*tmp$") then					-- Copy each valid Source file to Target file
									doCopyFile(strSourceFile,strTargetFile,strFunction,strFileName)
									intFileCount = intFileCount + 1
									doFileStatus(intFileCount,strFunction.." performed for ",strFileName)	-- V3.2
								end
							end
						end
					end
				end
			end
		end
		StrButton = nil																				-- Used to remember Delete/Keep All Files option below
		local intFileCheck = 0
		for _,tblAttr in ipairs (general.GetFolderContents(strTargetDir)) do				-- V3.6
			local strPath = tblAttr.path
			if tblAttr.mode == "directory" and not strPath:match("\\Map$") then 			-- Exclude large \Map... directory	-- V3.6
				local strDir = strPath:replace(strTargetDir,strSourceDir)					-- Ensure a root directory exists 	-- V3.6
				if not doMakeFolder(strDir,strFunction.." "..strProgName.." folder. ") then return -1 end
				for _,tblAttr in ipairs (general.GetFolderContents(strPath,true)) do		-- V3.6
					local strTargetFile = tblAttr.path
					if tblAttr.mode == "file" then
						local strSourceFile	 = strTargetFile:replace(strTargetDir,strSourceDir)
						local strFileName	 = strTargetFile:replace(strTargetDir.."\\","")
						local md5FileName
						if next(dicMd5ToPath) then
							if strFunction:match("Backup") then 									-- Map any md5 file to long path	-- V3.1
								md5FileName = strFileName
								strFileName = dicMd5ToPath[strFileName] or strFileName
								strSourceFile = strSourceDir.."\\"..strFileName
							elseif strFunction:match("Restore") then							-- Map any long path to md5 file	-- V3.1
								md5FileName = dicPathToMd5[strFileName] or strFileName
								strSourceFile = strSourceDir.."\\"..md5FileName
							end
						end
						if not strFileName:match("^Map\\Cache")									-- Exclude Map Cache that needs no backup
						and not strFileName:match("Backup and Restore Family Historian Settings%.[bd]at$") then	-- V3.2
							intFileCheck = intFileCheck + 1
							doFileStatus(intFileCheck,"Discrepancy check for ",strFileName)	-- V3.2
							if not general.FlgFileExists(strSourceFile) then					-- If target file has no matching source file, should it be deleted?
								local strButton = "Delete"
								if StrButton then
									strButton = StrButton
								else
									lblFilename.Title = strFileName:gsub("&","&&")				-- Ensure GUI shows current file	-- V3.2
									local strSourceData = dicDataMode[strFunction].Source		-- Adjust dialogue for Backup or Restore function -- V2.4
									local strTargetData = dicDataMode[strFunction].Target
									local strModified = tblAttr.modified							-- V3.2
									strButton = GUI_ModeDialogue(	"Delete File : "..strFileName,	-- V3.2
																			"WARNING: The file does not exist in the "..strSourceData..".\nShould the file dated "..strModified.." be deleted from the "..strTargetData.."?",
																			"Delete This File", "Keep This File", "Delete All Files", "Keep All Files"	)
									if strButton:match("All Files") then
										StrButton = strButton										-- Remember Delete/Keep All Files option
									end
								end
								if strButton:match("Delete") then
									general.DeleteFile(strTargetFile)								-- Delete any Target file that does not exist in Source (exclude root folder)
									if md5FileName then
										dicMd5ToPath[md5FileName] = nil							-- Remove from mapping tables	-- V3.1
										dicPathToMd5[strFileName] = nil
									end
								else
									intFileCount = intFileCount + 1
									lblStatus.Title = strFunction.." performed for "..intFileCount.." data files..."
								end
							end
						end
					end
				end
			end
		end
		return intFileCount
	end -- local function intCopyFiles

	local function doCheckUninstalledData()														-- Check for Uninstalled Data to Delete
		for _, arrAttr in ipairs ( general.GetFolderContents(StrBackupDir) ) do			-- V3.2
			if arrAttr.mode == "directory" then
				local strProgPath = arrAttr.path
				local strProgName = strProgPath:match("\\(Family Historian[^\\]-)$")		-- Folder name starts with Family Historian
				if strProgName then
					local intFolder = 0																-- Count standard Family Historian sub-folders	-- V3.2
					for _, arrAttr in ipairs ( general.GetFolderContents(StrCalicoPie.."\\Family Historian") ) do
						if arrAttr.mode == "directory" then
							local strFolder = arrAttr.path:match("[^\\]+$")
							if general.FlgFolderExists(strProgPath.."\\"..strFolder)
							or general.FlgFolderExists(strProgPath..StrProg_Data.."\\"..strFolder) then
								intFolder = intFolder + 1
							end
						end
					end
					if intFolder < 8 then															-- If too few then cannot be a Backup Folder
						strProgName = nil
					else
						for intProgram = 1, #TblProgram do
							if TblProgram[intProgram] == strProgName then						-- Signal a match with an installed Program
								strProgName = nil
							end	
						end
					end
					if strProgName then
						lblProgram.Title = strProgName
						local strButton = GUI_ModeDialogue(	"Program Uninstalled",
																	"ADVICE: "..strProgName.." is not installed so should its Backup be deleted?",
																	"Yes - Delete", "No - Keep"	)
						if strButton:match("Delete") then
							general.DeleteFolder(strProgPath)										-- Delete the Family Historian folder	-- V3.2
						end
					end
				end
			end
		end
	end -- local function doCheckUninstalledData

	local strKeyHKCU = "HKCU\\Software\\Calico Pie"											-- HKEY_CURRENT_USER keys path for REG
	local strKeyHKLM = "HKLM\\SOFTWARE\\Calico Pie"											-- HKEY_LOCAL_MACHINE keys path for REG
	if StrProcArch64 and fhGetAppVersion() < 8 then											-- Insert WOW6432Node for 64-bit PC and 32-bit FH  -- V3.2 -- V3.8
			strKeyHKLM = "HKLM\\SOFTWARE\\WOW6432Node\\Calico Pie"
	else
			tglLaterVer.Active = "NO"																-- FH V8 cannot accept earlier versions	-- V3.8
	end
	local strRegKeys = "Registry.keys"															-- Backup of Registry Keys filename
	local strVerData = "Version.data"																-- Backup of Version Data filename
	local strListTxt = "ImageList.txt"															-- List of External Images filename
	local strPathMap = "LongPath.txt"																-- Mapping of md5 filename to long path exceeding 250 chars

	--[=[
	HKEY_CURRENT_USER\Software\Calico Pie\Family Historian...													-- Current User keys for Diagrams, etc.
	HKEY_USERS\S-1-5-21-4109675573-1409003035-2569323029-1002\Software\Calico Pie\Family Historian...	-- Default User keys for above, not for Backup
	HKEY_LOCAL_MACHINE\SOFTWARE\{WOW6432Node\}Calico Pie\Family Historian... (WOW6432Node = 64-bit PC)	-- Local PC keys for Preferences, etc.
	]=]

	local function runBatchFileOK(strScript,isModal)											-- Invoke a Batch Script
		local intMode = 0																			-- 0 = Hide, 1 = Open, 2 = Minimise -- V2.9
		if isModal == nil then isModal = true end												-- Default to run Hidden in Modal wait mode
		if isModal then
			isModal = true																			-- Ensure isModal is set true
		else
			isModal = false																			-- If non-Modal the REG IMPORT batch file will Open Window & return
			intMode = 1
		end
		if StrDiagnose == "ON" then
			intMode = 1
			strScript = strScript:replace("{Diag}","PAUSE")									-- PAUSE in Diagnose mode to let user respond
		else
			strScript = strScript:gsub("\n\t*{Diag}","")
		end
		general.DeleteFile(StrRegFile)
		strScript = strScript:replace("{Reg}",StrRegFile)										-- Registry keys output file
		local luaShell = luacom.CreateObject("WScript.Shell")
		general.DeleteFile(StrBatFile)
		general.SaveStringToFile(strScript,StrBatFile,"ANSI")									-- V3.7
		luaShell:Run('cmd.exe /C \"'..StrBatFile..'\"', intMode, isModal)					-- intMode = 0 Hide or 1 Open, isModal = true waits, or false returns
		if not isModal then return true end														-- If non-Modal the REG IMPORT batch file must be kept
		if general.FlgFileExists(StrRegFile) then return true end							-- If Registry keys output then OK
		return false
	end -- local function runBatchFileOK

	local function doSetProgramVersions()														-- Set FH Program Names & Versions from Calico Pie data -- V2.0 variant
		local strRegQuery = 																		-- Script to run REG QUERY on HKLM /Subkeys for all Family Historian programs
		[=[
		@ECHO OFF
		ECHO.
		ECHO REG QUERY "{HKLM}" /S
		ECHO.
		REG QUERY "{HKLM}" /S > "{Reg}"
		(IF ERRORLEVEL 1 ECHO. PAUSE)
		ECHO.
		{Diag}
		EXIT
		]=]
		local strAppData = fhGetContextInfo("CI_APP_DATA_FOLDER")							-- V3.5
		if not general.FlgFolderExists(strAppData.."\\Plugin Data") then
				GUI_ModeDialogue(	"Plugin Data Error",
										"ERROR: "..strAppData.."\\Plugin Data folder is missing. \n Plugin cannot proceed.  Plugin will be closed.\n\n",
										"OK"	)
				return false
		end
		local strCurrent = strAppData:replace(StrCalicoPie.."\\","")
		local strProgram = ""
		local strVersion = ""
		local function isProgramCurrent() return strProgram:lower() == strCurrent:lower() end	-- Allow case to differ	-- V3.0
		lblFilename.Title = StrRegName
		strRegQuery = strRegQuery:replace("{HKLM}",strKeyHKLM)
		if runBatchFileOK(strRegQuery) then														-- Invoke the Batch Script file to REG QUERY HKLM keys
			for strLine in encoder.FileLines(StrRegFile,"UTF-8") do							-- Read lines from Unicode UTF-16 into UTF-8 -- V2.5
				local strProgKey = strLine:match(strKeyHKLM:gsub("HKLM","HKEY_LOCAL_MACHINE").."\\(.*)\\2.0$")
				if strProgKey and strProgKey:match("^Family Historian") then
					strProgram = strProgKey															-- Remember name of each Family Historian program in turn
					strVersion = " Version "
					if isProgramCurrent() then														-- V3.0
						table.insert(TblProgram,strProgram)										-- Add current running Family Historian program to end of table
					else
						table.insert(TblProgram,1,strProgram)									-- Add other installed Family Historian programs to top of table
					end
				end
				local strDigit = strLine:match("%s+Ver%d%s+REG_DWORD%s+0x(%x+)")			-- Cater for hex (base 16) version digits 0 - 9 & b - f	-- V2.1
				if strDigit then
					strVersion = strVersion..tonumber(strDigit,16).."."						-- Compose the Version number digits of each program		-- V2.1
				end
				local strBackups = strLine:match("%s+Backup Directory%s+REG_SZ%s+(.*)")
				if strBackups then
					strVersion = strVersion:gsub("%.$","")										-- Save the Version number of program minus trailing dot
					if isProgramCurrent() then														-- V3.0
						table.insert(TblVersion,strVersion)
						StrBackupKey = strBackups													-- Save the Backups folder of current program as default Backup Folder
					else
						table.insert(TblVersion,1,strVersion)
					end
				end
			end
			if StrDiagnose == "OFF" then
				general.DeleteFile(StrRegFile)
			end
			lblFilename.Title = " "
		else
			local intSize = intSize(StrRegFile,"No")											-- V3.2
			GUI_ModeDialogue(	"Registry Query Error",
									"ERROR: Registry Key data not saved by REG QUERY "..strKeyHKLM.."\n "..intSize.." bytes written to "..StrRegFile.."\n Plugin cannot proceed.  Plugin will be closed.\n\n",
									"OK"	)
			return false
		end
		return true
	end -- local function doSetProgramVersions

	local function doWriteRegistry(tblRegFile,strRegFile)										-- Write the Registry.keys file in Unicode UTF-16 -- V2.6
		local strReg = table.concat(tblRegFile,"\r\n").."\r\n"
		if fhGetAppVersion() > 6 then
			general.SaveStringToFile(strReg,strRegFile,"UTF-16LE")							-- Save the Registry.keys in Unicode UTF-16 -- V3.2
		else
			strReg = encoder.StrUTF8_UTF16(strReg)
			local putReg = general.OpenFile(StrRegFile,"wb")									-- Save the Registry.keys in Unicode UTF-16 -- V2.4
			putReg:write(string.char(0xFF,0xFE)..strReg)										-- Prefix with UTF-16 BOM
			putReg:close()
			general.MoveFile(StrRegFile,strRegFile)												-- Cater for Unicode file path	-- V3.2
		end
	end -- local function doWriteRegistry

	local function doExportRegistry(strTargetDir,strProgName)								-- Invoke a Batch Script to Export the Registry Keys and find external image files -- V2.0 variant
		--[=[
			The FIND commands search for ":\" and "\\" to locate file paths like "C:\..." and "\\NAME\..." in Standard & Custom Diagram Type .fhd/fhdx files	-- "\\NAME\" supported 1 Feb 2014
			Windows 7 & Vista support REG QUERY "KeyPath" /V "Value" /S but on Windows XP /V and /S are mutually exclusive
			Thus the following script works on Windows 7 & Vista but not on Windows XP - the resulting side effect is that the output file is a bit bigger
			IF EXIST "{Targ}\Image.Icons" DEL "{Targ}\Image.Icons"
			REG QUERY "{HKCU}" /V "Icon" /S > "{Targ}\Image.Icons"
			IF EXIST "{Targ}\Image.Picts" DEL "{Targ}\Image.Picts"
			REG QUERY "{HKCU}" /V "Bkg Pic Path" /S > "{Targ}\Image.Picts"
		]=]
		local strRegExport =			 															-- Script to run FIND & REG QUERY / EXPORT on HKCU & HKLM keys for one Family Historian program
		[=[
		@ECHO OFF
		(IF EXIST "{Targ}\Image.StandardDrive" DEL "{Targ}\Image.StandardDrive")
		(IF EXIST "{Targ}\Image.StandardName" DEL "{Targ}\Image.StandardName")
		(IF EXIST "{Targ}{Prog}\Diagrams\Standard\*.fhd*" FIND ":\" "{Targ}{Prog}\Diagrams\Standard\*.fhd*" > "{Targ}\Image.StandardDrive" FIND "\\" "{Targ}{Prog}\Diagrams\Standard\*.fhd*" > "{Targ}\Image.StandardName")
		(IF EXIST "{Targ}\Image.CustomDrive" DEL "{Targ}\Image.CustomDrive")
		(IF EXIST "{Targ}\Image.CustomName" DEL "{Targ}\Image.CustomName")
		(IF EXIST "{Targ}{Prog}\Diagrams\Custom\*.fhd*" FIND ":\" "{Targ}{Prog}\Diagrams\Custom\*.fhd*" > "{Targ}\Image.CustomDrive" FIND "\\" "{Targ}{Prog}\Diagrams\Custom\*.fhd*" > "{Targ}\Image.CustomName")
		ECHO.
		ECHO {Find}
		ECHO.
		(IF EXIST "{Targ}\Image.Query" DEL "{Targ}\Image.Query")
		{Find} > "{Targ}\Image.Query"
		ECHO.
		(IF EXIST "{Reg}" DEL "{Reg}")
		(IF EXIST "{Reg}-1" DEL "{Reg}-1")
		(IF EXIST "{Reg}-2" DEL "{Reg}-2")
		ECHO.
		ECHO {Reg1}
		ECHO.
		{Reg1}
		(IF ERRORLEVEL 1 ECHO. PAUSE)
		ECHO.
		ECHO {Reg2}
		ECHO.
		{Reg2}
		(IF ERRORLEVEL 1 ECHO. PAUSE)
		COPY "{Reg}-?" "{Reg}"
		DEL "{Reg}-?"
		ECHO.
		{Diag}
		EXIT
		]=]
		local flgKeyHKLM = false																	-- Invoke the Batch Script file to FIND files & REG QUERY / EXPORT HKCU & HKLM keys 
		local tblRegFile = {}
		local strHKCU = strKeyHKCU.."\\"..strProgName
		local strHKLM = strKeyHKLM.."\\"..strProgName.."\\2.0\\Preferences"
		strRegExport = strRegExport:replace("{Find}",'REG QUERY "{HKCU}" /S')
		strRegExport = strRegExport:replace("{Reg1}",'REG EXPORT "{HKCU}" "{Reg}-1"')
		strRegExport = strRegExport:replace("{Reg2}",'REG EXPORT "{HKLM}" "{Reg}-2"')
		strRegExport = strRegExport:replace("{HKCU}",strHKCU)
		strRegExport = strRegExport:replace("{HKLM}",strHKLM)
		strRegExport = strRegExport:replace("{Targ}",strTargetDir)
		strRegExport = strRegExport:replace("{Prog}",StrProg_Data)
		lblFilename.Title = StrRegName
		if not runBatchFileOK(strRegExport) then
			GUI_ModeDialogue(	"Registry Export Error",
									"ERROR: Registry Key data not saved by REG EXPORT for\n"..strHKCU.."\n"..strHKLM.."\n"..StrRegFile.."\n",
									"OK"	)
			return
		end
		for strLine in encoder.FileLines(StrRegFile,"UTF-8") do								-- Read lines from Unicode UTF-16 into UTF-8 -- V2.4
			if strLine:match("HKEY_LOCAL_MACHINE") then flgKeyHKLM = true end
			if not ( flgKeyHKLM and ( strLine:match("=\"%u:\\\\") or strLine:match("=\"\\\\\\\\.-\\\\") ) ) then
				table.insert(tblRegFile,strLine)													-- Copy Registry Keys excluding HKLM file references such as ="C:\\ or ="\\\\NAME\\
			end
		end
		if StrDiagnose == "OFF" then
			general.DeleteFile(StrBatFile)
			general.DeleteFile(StrRegFile)
		end
		doWriteRegistry(tblRegFile,strTargetDir.."\\"..strRegKeys)							-- Save Registry.keys in Unicode UTF-16 -- V2.6
	end -- local function doExportRegistry

	local function doMakeVbsImport(strRegFile)													-- Compose VBS file to import Registry Data file
		local strHkey = ""
		local arrVbs = {}																			-- VBS array of commands
		table.insert(arrVbs,'Dim WshShell')
		table.insert(arrVbs,'Set WshShell = WScript.CreateObject("WScript.Shell")')
		for strLine in encoder.FileLines(strRegFile,"UTF-8") do								-- Read lines from Unicode UTF-16 into UTF-8
			local strKey = strLine:match("^%[(HKEY_.+)]$")
			if strKey then strHkey = strKey.."\\" end											-- Remember current Registry Key
			local strName, strData = strLine:match('^"(.+)"=(.+)$')
			if strName and strData then															-- Found some Registry Key Data
				local strType = ',"REG_SZ"'
				local strWord = strData:match('^dword:(%x+)$')
				if strWord then
					strData = tostring(tonumber(strWord,16))									-- Convert dword: in hex to decimal
					strType = ',"REG_DWORD"'
				end
				table.insert(arrVbs,'WshShell.RegWrite "'..strHkey..strName..'",'..strData..strType)
			end
		end
		general.DeleteFile(StrVbsFile)
		general.SaveStringToFile(table.concat(arrVbs,"\n").."\n",StrVbsFile)				-- Save the VBS command file
	end -- local function doMakeVbsImport

	local function doImportRegistry(strSourceDir,strProgName)								-- Invoke a Batch Script to Import the Registry Keys -- V2.0 variant
		local strAskChoice = 																		-- Script to ask before continuing with the REG IMPORT script below
		[=[
		@ECHO OFF
		ECHO.
		ECHO {Copy1}
		ECHO.
		ECHO {Copy2}
		ECHO.
		ECHO {Mode}
		ECHO.
		ECHO ~ Ensure the Family Historian program is closed before continuing ~ 
		ECHO.
	:REPEAT
		ECHO.
		SET CHOICE=
		SET /P CHOICE=~ Do you want to continue and import Registry Data Keys [Y/N] ? 
		(IF NOT "%CHOICE%" EQU "" SET CHOICE=%CHOICE:~0,1%)
		(IF /I "%CHOICE%" EQU "N" EXIT)
		(IF /I "%CHOICE%" NEQ "Y" GOTO REPEAT)
		ECHO.
		]=]
		local strRegImport = 																		-- Script to run REG IMPORT on one Family Historian backup Registry.keys file and copy files
		[=[
		@ECHO OFF
		ECHO.
		ECHO {Copy1}
		ECHO.
		{Copy1}
		ECHO.
		ECHO {Copy2}
		ECHO.
		{Copy2}
		ECHO.
		ECHO {Mode}
		ECHO.
		{Mode}
		(IF ERRORLEVEL 1 ECHO. PAUSE)
		ECHO.
		{Diag}
		EXIT
		]=]
		local strRegFile = strSourceDir.."\\"..strRegKeys
		lblFilename.Title = strRegKeys
		if general.FlgFileExists(strRegFile) then

			local function doBatchJob(strRegImport,isModal)									-- Invoke the Batch Script file to COPY prefs.dat and REG IMPORT registry keys
				local strPreferPref = "\\Preferences\\prefs.dat"								-- Check prefs.dat file that FH overwrites on closure	-- V2.5
				local strSourcePref = strSourceDir..StrUser_Data..strPreferPref				-- Backup version
				local strTargetPref = StrApplicPie.."\\"..strProgName..strPreferPref		-- AppData version
				local intSourceTime = intTime(strSourcePref,-1)								-- V3.2
				local intTargetTime = intTime(strTargetPref,-9)
				if intSourceTime ~= intTargetTime then											-- prefs.dat has been restored so COPY again with FH closed	-- V2.5
					strRegImport = strRegImport:replace("{Copy1}",'COPY "'..strSourcePref..'" "'..strTargetPref..'"')
				else
					strRegImport = strRegImport:replace("{Copy1}","")
				end
				local strGroupIndex = "\\Fact Types\\Standard\\GroupIndex.fhdata"			-- Check GroupIndex.fhdata file that FH overwrites on closure	-- V3.0
				local strSourcePref = strSourceDir..StrProg_Data..strGroupIndex				-- Backup version
				local strTargetPref = StrCalicoPie.."\\"..strProgName..strGroupIndex		-- AppData version
				local intSourceTime = intTime(strSourcePref,-1)								-- V3.2
				local intTargetTime = intTime(strTargetPref,-9)
				if intSourceTime ~= intTargetTime then											-- GroupIndex.fhdata has been restored so COPY again with FH closed	-- V3.0
					strRegImport = strRegImport:replace("{Copy2}",'COPY "'..strSourcePref..'" "'..strTargetPref..'"')
				else
					strRegImport = strRegImport:replace("{Copy2}","")
				end
				strRegImport = strRegImport:replace("{Mode}",'REG IMPORT "'..strRegFile..'"')
				runBatchFileOK(strRegImport,isModal)
				lblStatus.Title = "Restore initialised for 1 Registry file"
				fhSleep(1000,200)
			end -- local function doBatchJob

			local tblRegFile = {}																	-- Ensure the Registry keys match FH program -- V2.6
			local intRegFile = 0
			general.CopyFile(strRegFile,StrRegFile)												-- Cater for Unicode file path	-- V3.2
			for strLine in encoder.FileLines(StrRegFile,"UTF-8") do							-- Read lines from Unicode UTF-16 into UTF-8 -- V2.4 -- V3.2
				local strKey, intKey = strLine:gsub("^(%[HKEY_.-\\Calico Pie\\)Family Historian.-([\\%]])","%1"..strProgName.."%2")
				if intKey > 0 and strKey ~= strLine then
					intRegFile = intRegFile + intKey
					strLine = strKey
				end
				table.insert(tblRegFile,strLine)
			end
			general.DeleteFile(StrRegFile)														-- V3.2
			if intRegFile > 0 then																	-- Adjust the Registry.keys file to match program -- V2.6
				doWriteRegistry(tblRegFile,strRegFile)											-- Save the Registry.keys file in Unicode UTF-16 -- V2.6
			end
			if strProgName == TblProgram[#TblProgram] then										-- Handle currently running program differently
				local strButton = GUI_ModeDialogue(	"Close Program",
															"WARNING: Ensure '"..lblProgram.Title.."' is closed before restoring its Registry Keys data.\nOtherwise the restored Registry Keys will be overwritten when the program is closed.\nSo after clicking 'OK - Quit Plugin' the Plugin will close to let you close the program.\nThe 'REG IMPORT' dialogue will remain open after '"..strProgName.."' is closed.",
															"OK - Quit Plugin", "No - Close Later"	)
				doBatchJob(strAskChoice..strRegImport,false)									-- Invoke the Batch Script file to ask to import registry keys non-Modal
				if strButton:match("OK") then
					return -1
				end
			else
				local strButton = GUI_ModeDialogue(	"Close Program",
															"WARNING: Ensure '"..lblProgram.Title.."' is closed before restoring its Registry Keys data.\nOtherwise the restored Registry Keys will be overwritten when the program is closed.\nThis is the last opportunity to quit restoring the Registry Keys data.",
															"OK - Program Closed", "Quit Restore Registry"	)
				if strButton:match("OK") then
					doBatchJob(strRegImport)														-- Invoke the Batch Script file to import registry keys
					if StrDiagnose == "OFF" then
						general.DeleteFile(StrBatFile)
						general.DeleteFile(StrRegFile)
						general.DeleteFile(StrVbsFile)
					end
				end
			end
		else
			GUI_ModeDialogue(	"Backup File Missing",
									"ERROR: Cannot restore from missing backup registry file:\n"..strRegFile.."\n",
									"OK"	)
		end
		return 0
	end -- local function doImportRegistry

	local function intBackupImageFiles(strProgramDir,strTargetDir)							-- Backup image files not located in Program Data folders
		local strFileList = ""
		local strFileDiag = nil
		local strFilePath = nil
		local tblFilePath = {}
		local intFilePath = 0
		local intFileCount = 0
		local strMatch = nil																		-- Search the Image.* files created by doExportRegistry() above
		for i, strFileType in ipairs( { "StandardDrive";"StandardName";"CustomDrive";"CustomName";"Query"; } ) do	-- 1 Feb 2014 V2.0
			local strImageFile = strTargetDir.."\\Image."..strFileType
			if general.FlgFileExists(strImageFile) then											-- Cater for both C:\ and \\NAME\ style leading path names
				local strList = general.StrLoadFromFile(strImageFile)							-- V3.7
				for strLine in strList:gmatch("[^\r\n]+") do									-- Look through the ImageFile to find image file paths
					strMatch = strLine:match("^---------- %u:\\.*\\(STANDARD\\.*)$")		-- Standard Diagram Type (.fhd/.fhdx) definition file
							or strLine:match("^---------- \\\\.-\\.*\\(STANDARD\\.*)$")
					if strMatch then strFileDiag = strMatch end
					strMatch = strLine:match("^---------- %u:\\.*\\(CUSTOM\\.*)$")			-- Custom Diagram Type (.fhd/.fhdx) definition file
							or strLine:match("^---------- \\\\.-\\.*\\(CUSTOM\\.*)$")
					if strMatch then strFileDiag = strMatch end
					strMatch = strLine:match("^Icon=(%u:\\.*)$")								-- Diagram Type (.fhd) Box Icon external image file
							or strLine:match("^Icon=(\\\\.-\\.*)$")
					if strMatch then strFilePath = strMatch end
					strMatch = strLine:match("^(%u:\\.-)[^%a]*$")								-- Diagram Type (.fhdx) Picture or Box Icon external image file
							or strLine:match("^(\\\\.-\\.-)[^%a]*$")
					if strMatch then strFilePath = strMatch end
					strMatch = strLine:match("%s*Bkg Pic Path%s*REG_SZ%s*(%u:\\.*)$")		-- Registry Diagram Picture external image file
							or strLine:match("%s*Bkg Pic Path%s*REG_SZ%s*(\\\\.-\\.*)$")
					if strMatch then strFileDiag = "REGISTRY DIAGRAM PICTURE" strFilePath = strMatch end
					strMatch = strLine:match("%s*Icon%s*REG_SZ%s*(%u:\\.*)$")				-- Registry Diagram Box Icon external image file
							or strLine:match("%s*Icon%s*REG_SZ%s*(\\\\.-\\.*)$")
					if strMatch then strFileDiag = "REGISTRY DIAGRAM BOX ICON" strFilePath = strMatch end
					if strFilePath and
					not strFilePath:match(strProgramDir..".-\\.-\\") then						-- Image file path not in any Program Data subfolder
						local intFileIndex = tblFilePath[strFilePath]
						if not intFileIndex then
							intFilePath = intFilePath + 1
							intFileIndex = intFilePath
							tblFilePath[strFilePath] = intFilePath
							if general.FlgFileExists(strFilePath) then
								intFileCount = intFileCount + 1
								local strFileName = "\\"..intFileIndex.."~"..( strFilePath:match("%u:\\.-([^\\]-[^%.]+)$") or strFilePath:match("\\\\.-\\.-([^\\]-[^%.]+)$") )
								doCopyFile(strFilePath,strTargetDir..strFileName,"Backup")
								tblFilePath[strFileName] = intFilePath
							end
						end
						local strFileText = "  references missing file  "
						if general.FlgFileExists(strFilePath) then
							strFileText = "  references file "..intFileIndex.."  ~  "
						end
						strFileList = strFileList..strFileDiag..strFileText..strFilePath.."\n"
						strFilePath = nil
					end
				end
				if StrDiagnose == "OFF" then
					general.DeleteFile(strImageFile)
				end
			end
		end
		for _, arrAttr in ipairs ( general.GetFolderContents(strTargetDir) ) do			-- Search backup folder for image files	-- V3.2
			if arrAttr.mode == "file" then
				local strFilePath = arrAttr.path
				local strFileName = strFilePath:match("\\[^\\]+$")
				if strFileName:match("^\\%d+~") and not tblFilePath[strFileName] then
					general.DeleteFile(strFilePath)												-- Delete redundant old image file backup	-- V3.2
				end
			end
		end
		if strFileList ~= "" then
			local strImageList = strTargetDir.."\\"..strListTxt
			general.SaveStringToFile(strFileList,strImageList)
			lblFilename.Title = strListTxt:gsub("&","&&")
			local strMoveAdvice = ""
			if intFileCount > 0 then strMoveAdvice = "\nPreferably any Box Icon external images should be moved to the Program Data FH Program Icons folder." end
			local strMissAdvice = ""
			local intMissCount = intFilePath - intFileCount
			if intMissCount > 0 then strMissAdvice = "\nTake note that "..intMissCount.." of the referenced external image files cannot be found." end
			local strButton = GUI_ModeDialogue(	"External Image File",
														"ADVICE: Diagram Type Picture / Box Icon external image file references are listed in the "..strListTxt.." file."..strMoveAdvice..strMissAdvice,
														"View Image List", "OK - Continue"	)
			if strButton:match("View") then DoExecute( strImageList, " " ) end
		end
		return intFileCount
	end -- local function intBackupImageFiles

	local function intRestoreImageFiles(strSourceDir)											-- Restore image files not located in Program Data folders
		local intFileCount = 0
		local intFileIndex = 0
		local intFileError = 0
		local strTargetDir = ""
		local strImageList = strSourceDir.."\\"..strListTxt
		if general.FlgFileExists(strImageList) then
			lblFilename.Title = strListTxt:gsub("&","&&")
			local strList = general.StrLoadFromFile(strImageList)								-- V3.7
			for strLine in strList:gmatch("[^\r\n]+") do										-- Look through the FileList.txt file to find image file paths with either C:\ or \\NAME\ style
				local intFilePath, strFilePath = strLine:match(".*  references file (%d*)  ~  (.*)$")
				if strFilePath then
					local strFileName = strSourceDir.."\\"..intFilePath.."~"..( strFilePath:match("%u:\\.-([^\\]-[^%.]+)$") or strFilePath:match("\\\\.-\\.-([^\\]-[^%.]+)$") )
					intFilePath = tonumber(intFilePath)
					if general.FlgFileExists(strFileName) and intFilePath > intFileIndex then
						intFileCount = intFileCount + 1
						intFileIndex = intFilePath
						strTargetDir = general.SplitFilename(strFilePath)
						if general.FlgFolderExists(strTargetDir) then
							doCopyFile(strFileName,strFilePath,"Restore")
						else
							if doMakeFolder(strTargetDir,"Restore Image folder path. ") then	-- Ensure the Target folder exists -- V2.0 variant
								doCopyFile(strFileName,strFilePath,"Restore")
							else
								intFileError = intFileError + 1
							end
						end
					end
				end
			end
			if intFileCount > 0 then
				local strButton = GUI_ModeDialogue(	"External Image File",
															"ADVICE: Diagram Type Picture / Box Icon external image file references are listed in the "..strListTxt.." file.\nPreferably any Box Icon external images should be moved to the Program Data FH Program Icons folder.",
															"View Image List", "OK - Continue"	)
				if strButton:match("View") then DoExecute( strImageList, " " ) end
			end
		end
		return intFileCount - intFileError
	end -- local function intRestoreImageFiles

	local function doRenameFolder(strSource,strTarget)										-- Rename folder from Source to Target if Source exists
		if general.FlgFolderExists(strSource) then
			general.DeleteFile(strTarget)
			local isOK, strErrorText = os.rename(strSource,strTarget)
			if not isOK then
				GUI_ModeDialogue(	"Rename Failed",
										"ERROR: Rename of folder failed for:\n"..strSource.."\n"..strTarget.."\n",
										"OK"	)
			end
		end
	end -- local function doRenameFolder

	local strTodaysDate = os.date("%Y-%m-%d %H%M") 	-- Use %H%M for Hours & Mins		-- 19 July 2013

	local function strRemoveDatedDir(strBackupDir)												-- 19 July 2013
		strBackupDir = strBackupDir:gsub("\\FH Settings 2%d%d%d%-%d%d%-%d%d ?%d-$","")
		return strBackupDir
	end -- local function strRemoveDatedDir

	local function strAppendDatedDir(strBackupDir)												-- 19 July 2013
		strBackupDir = strRemoveDatedDir(strBackupDir).."\\FH Settings "..strTodaysDate
		return strBackupDir
	end -- local function strAppendDatedDir

	local function setBackupFolderOK(strFunction,strBackupDir)								-- Choose Backup/Restore Folder Path

		local function makeSubFolder()															-- 19 July 2013
			if strFunction == "Backup" and StrDatedDir == "ON" then
				strBackupDir = strAppendDatedDir(strBackupDir)									-- Must create subfolder -- V2.0 variant
				if not doMakeFolder(strBackupDir,"Backup Date-Time subfolder. ") then return false end
			end
			StrBackupDir = strBackupDir
			SaveSettings()																			-- Save sticky data settings
			return true
		end -- local function makeSubFolder

		local function okBackupDir(strBackupDir)												-- Detect \Family Historian...\Program Data and advise using parent -- V2.9
			if strBackupDir:match("\\Family Historian[^\\]-$")
			and general.FlgFolderExists(strBackupDir.."\\Program Data") then
				strBackupDir = strBackupDir:gsub("\\Family Historian[^\\]-$","")
				GUI_ModeDialogue(	strFunction.." Cancelled",
										"ADVICE: "..strFunction.." Operation has failed with invalid folder. Try choosing parent folder: \n\n "..strBackupDir,
										"OK"	)
				return false
			end
			return true
		end -- local function okBackupDir

		if strFunction == "Backup" then strBackupDir = strRemoveDatedDir(strBackupDir) end -- 19 July 2013
		if FlgFolderWrite(strBackupDir) then														-- V2.9 -- If supplied folder writeable then dialogue not required
			if not okBackupDir(strBackupDir) then return false end 							-- V2.9
			return makeSubFolder()
		end
		if general.FlgFolderExists(StrBackupDir) then
			strBackupDir = StrBackupDir															-- Set initial directory to existing Plugin Backup Folder 
		else
			strBackupDir = StrBackupKey															-- Set initial directory to Program Preferences Backups Folder
		end
		if not FlgFolderWrite(strBackupDir) then												-- V2.9
			strBackupDir = StrPublicPath															-- Set initial directory to Project Public folder
		end
		local filedlg = iup.filedlg{ dialogtype="DIR"; Title="The current Backup Folder path is \n"..strBackupDir; directory=strBackupDir; } -- V3.0
		filedlg:popup(iup.CENTER,iup.CENTER)
		if filedlg.status == "0" then
			strBackupDir = filedlg.value
			if FlgFolderWrite(strBackupDir) then													-- V2.9 -- If chosen folder is writeable then all OK
				if not okBackupDir(strBackupDir) then return false end 						-- V2.9
				return makeSubFolder()																-- 19 July 2013
			else
				lblBackData.Title = strBackupDir:gsub("&","&&")
				GUI_ModeDialogue(	strFunction.." Cancelled",
										"ERROR: "..strFunction.." Operation has failed because the Backup Folder does not have write access.", -- V3.0
										"OK"	)
				lblBackData.Title = StrBackupDir:gsub("&","&&")
				return false
			end
		else
			GUI_ModeDialogue(	strFunction.." Cancelled",
									"ADVICE: "..strFunction.." Operation has been cancelled via the 'Browse For Folder' dialogue.",
									"OK"	)
		end
		return false		
	end -- local function setBackupFolderOK

	local function doCheckBackupDataFolders(strBackupDir)										-- Ensure backup Program Data and Application subfolders exist -- V2.4
		if  doMakeFolder(strBackupDir..StrProg_Data,"Program Data folder. ")				-- Create backup Program Data subfolder	-- V3.2
		and doMakeFolder(strBackupDir..StrUser_Data,"Application folder. ") then			-- Create backup Application subfolder 	-- V3.2 -- V2.4
			return true
		end
		return false
	end -- local function doCheckBackupDataFolders

	local function showFolderPaths(strBackData,strProgData)									-- Update status lblBackData.Title and lblProgData.Title		-- V2.0
		lblBackData.Title = strBackData:gsub("&","&&")
		lblProgData.Title = strProgData:gsub("&","&&")
	end -- local function showFolderPaths

	local function setMainControlsActive(strMode)												-- Enable/Disable controls during main tab actions		-- V2.0
		tabControls.Active = strMode
		btnDestroy.Active = strMode
	end -- local function setMainControlsActive

	local function showCompletedStatus(strMode,intFiles,intProgs)							-- V2.5
		showFolderPaths(StrBackupDir,StrCalicoPie)												-- V2.0
		doCheckUninstalledData()																	-- Check if any Backup Folder exists for an uninstalled program
		lblProgram.Title = "Family Historian ..."
		local strFiles = " files for "
		local strProgs = " programs"
		if intFiles == 1 then strFiles = " file for " end										-- V2.5
		if intProgs == 1 then strProgs = " program" end
		lblStatus.Title = strMode.." completed for "..intFiles..strFiles..intProgs..strProgs
	end -- local function showCompletedStatus

	local function loadMd5PathMap(strDir)														-- Load the md5 to long path map file	-- V3.1 -- V3.2
		dicMd5ToPath = {}
		dicPathToMd5 = {}
		local strMapFile = strDir.."\\"..strPathMap
		if general.FlgFileExists(strMapFile) then 												-- Read the md5 to long path map file
			local strMd5ToPath = general.StrLoadFromFile(strMapFile)
			for strMd5,strPath in strMd5ToPath:gmatch("(%x+)=([^\r\n]+)[\r\n]+") do		-- md5 short filename = long file path exceeding 250 chars
				dicMd5ToPath[strMd5]  = strPath
				dicPathToMd5[strPath] = strMd5
			end
		end
	end -- local function loadMd5PathMap

	local function saveMd5PathMap(strDir)														-- Save the md5 to long path map file	-- V3.1 -- V3.2
		local strMapFile = strDir.."\\"..strPathMap
		if next(dicMd5ToPath) then
			local arrMd5ToPath = {}
			for strMd5,strPath in pairs(dicMd5ToPath) do
				table.insert(arrMd5ToPath,strMd5.."="..strPath.."\n")							-- md5 short filename = long file path exceeding 250 chars
			end
			general.SaveStringToFile(table.concat(arrMd5ToPath),strMapFile)					-- Save the md5 to long path map file (replaces \n with \r\n)
			dicMd5ToPath = {}
			dicPathToMd5 = {}
		else
			general.DeleteFile(strMapFile)														-- Delete any redundant path map file	-- V3.1
		end
	end -- local function saveMd5PathMap

	-- GUI Backup Tab Functions --

	function btnBackup:action()																	-- Action for Backup Customisations button
		setMainControlsActive("NO")
		if setBackupFolderOK("Backup",StrBackupDir) then										-- Ensure the Backup Folder is OK
			local intProgTotal = 0
			local intFileTotal = 0
			for intProgram = 1, #TblProgram do													-- Repeat for each installed Family Historian program Version
				local strProgName = TblProgram[intProgram]
				local strProgVers = TblVersion[intProgram]
				lblProgram.Title = strProgName..strProgVers
				lblFilename.Title = " "
				lblStatus.Title	= " "
				local strTargetDir = StrBackupDir.."\\"..strProgName							-- Backup target is Backup Folder with same program name
				local strSourceDir = StrCalicoPie.."\\"..strProgName							-- Backup source is Calico Pie program name data folder
				local strApplicPie = StrApplicPie.."\\"..strProgName							-- Backup source is Calico Pie plugin data files folder	-- 11 Feb 2014
				local strPublicPie = StrPublicPie.."\\"..strProgName							-- Backup source is Calico Pie tutorial files folder
				local strButton = "Yes"
				showFolderPaths(strTargetDir,strSourceDir)										-- V3.0
				IntBackupMod = 0
				if general.FlgFolderExists(strTargetDir) then
					if not doCheckBackupDataFolders(strTargetDir) then break end			-- Ensure Program Data and Application subfolders exist
					local strModified = ""
					local strVerFile = strTargetDir.."\\"..strVerData
					if general.FlgFileExists(strVerFile) then
						IntBackupMod, strModified = intTime(strVerFile,0)						-- Backup Folder modified date-time-stamp -- V3.2 -- V3.4
					end
					strButton = GUI_ModeDialogue(	"Backup Data Exists "..strModified,
															"WARNING: Backup Folder already has "..lblProgram.Title.." backup data saved.\nIs it acceptable to update this old Backup with a new Backup?",
															"Yes - Backup", "No - Skip"	)
				else
					strButton = GUI_ModeDialogue(	"Backup Data Request",
															"ADVICE: Is it acceptable to create a new Backup for "..lblProgram.Title.."?",
															"Yes - Backup", "No - Skip"	)
				end
				if strButton:match("Yes") then													-- Backup all Program Data files from Calico Pie folder with Application Plugin Data and Tutorial Files
					showFolderPaths(strTargetDir..StrProg_Data,strSourceDir)					-- V2.0
					if not doMakeFolder(strTargetDir,"Backup "..strProgName.." folder. ") then break end	-- Must create backup folder -- V2.0 variant
					loadMd5PathMap(strTargetDir)													-- Load the md5 to long file path map	-- V3.1
					local intFileCount = intCopyFiles(strSourceDir,strTargetDir..StrProg_Data,strProgName,"Backup",150)
					if intFileCount < 0 then break end
					local intUser_Data = 0
					if general.FlgFolderExists(strApplicPie) then								-- V2.4
						showFolderPaths(strTargetDir..StrUser_Data,strApplicPie)
						intUser_Data = intCopyFiles(strApplicPie,strTargetDir..StrUser_Data,strProgName,"Backup",0)
					end
					if intUser_Data < 0 then break end
					showFolderPaths(strTargetDir..StrTutorials,strPublicPie..StrTutorials)-- V2.0
					local intMin = 12
					if strProgVers >= " Version 6.0.0.0" then intMin = 0 end					-- No tutorials from FH V6 -- V2.2 -- V2.4 -- V2.5
					local intTutorials = intCopyFiles(strPublicPie..StrTutorials,strTargetDir..StrTutorials,strProgName,"Backup",intMin)
					if intTutorials < 0 then break end
					showFolderPaths(strTargetDir,strSourceDir)									-- V2.0
					intFileCount = intFileCount + intUser_Data + intTutorials
					if intFileCount > 0 then
						intProgTotal = intProgTotal + 1
						general.SaveStringToFile(strProgVers,strTargetDir.."\\"..strVerData)	-- Save the Program Version number
						doExportRegistry(strTargetDir,strProgName)								-- Export all Registry Keys for Program
						saveMd5PathMap(strTargetDir)												-- Save the md5 to long file path map	-- V3.1
						intFileCount = intFileCount + intBackupImageFiles(strSourceDir,strTargetDir)
						intFileTotal = intFileTotal + intFileCount
						lblFilename.Title = " "
						lblStatus.Title = "Backup completed for "..intFileCount.." data files and 1 registry file"
						fhSleep(1000,200)
					end
				end
			end -- per installed Program loop
			showCompletedStatus("Backup",intFileTotal,intProgTotal)							-- V2.5
		end
		setMainControlsActive("YES")
	end -- function btnBackup:action

	-- GUI Restore Tab Functions --

	local function intVersion(strVersion)														-- Convert first three digits of Version to equivalent number -- V2.6
		local strDig1, strDig2, strDig3 = strVersion:match("^ Version (%d+)%.(%d+)%.(%d+)")
		return tonumber( ( (strDig1 or 0) * 100 + (strDig2 or 0) ) * 100 + (strDig3 or 0) )
	end -- local function intVersion

	function btnRestore:action()																	-- Action for Restore Customisations button
		setMainControlsActive("NO")
		if setBackupFolderOK("Restore",StrBackupDir) then										-- Ensure the Backup Folder is OK
			local intProgTotal	= 0
			local intFileTotal	= 0
			for intProgram = 1, #TblProgram do
				local strProgName	= TblProgram[intProgram]									-- Repeat for each installed Family Historian program Version
				local strProgVers	= TblVersion[intProgram]
				lblProgram.Title 	= strProgName..strProgVers
				local strSourceDir	= StrBackupDir.."\\"..strProgName							-- Restore source is Backup Folder program name folder
				local strTargetDir	= StrCalicoPie.."\\"..strProgName							-- Restore target is Calico Pie program name data folder
				local strApplicPie	= StrApplicPie.."\\"..strProgName							-- Restore target is Calico Pie plugin data files folder	-- 11 Feb 2014
				local strPublicPie	= StrPublicPie.."\\"..strProgName							-- Restore target is Calico Pie tutorial files folder
				local strProgram		= "Program Data Files"
				local strTutorial	= StrTutorFiles
				local strRegistry	= StrRegDatKeys
				local intFileCount	= 0
				local strButton		= ""															-- V2.5
				showFolderPaths(strSourceDir,strTargetDir)										-- V3.0
				if general.FlgFolderExists(strSourceDir) then
					if not doCheckBackupDataFolders(strSourceDir) then break end			-- Ensure Program Data and Application subfolders exist
					repeat
						showFolderPaths(strSourceDir,strTargetDir)								-- V2.0
						lblFilename.Title = " "
						lblStatus.Title	= " "
						local strRegFile = strSourceDir.."\\"..strRegKeys
						local strVerFile = strSourceDir.."\\"..strVerData
						if not general.FlgFileExists(strVerFile) then
							GUI_ModeDialogue(	"Version Data Missing",
													"ERROR: Cannot find the "..lblProgram.Title.." program Version in the Backup Folder.\n"..strVerFile.." is missing.\n",
													"OK"	)
							break
						end
						local strBackVers = general.StrLoadFromFile(strVerFile)
						if strBackVers then
							strBackVers = strBackVers:gsub("[\r\n]+","")						-- V3.2
							local intProgVers = intVersion(strProgVers)							-- V2.6
							local intBackVers = intVersion(strBackVers)
							if intBackVers ~= intProgVers and strButton == "" then			-- V2.6 Only check first three digits
								local strBeware = "\nBut see '"..strRelaxFhVers.."' on Options tab."
								local strIgnore = ""
								if StrLaterVer == "ON" and intBackVers < intProgVers then	-- V2.6
									strBeware = "\nAlternatively, mismatch will be ignored if you click 'Ignore'.  BEWARE: Use entirely at your own risk !"	-- V2.5
									strIgnore = "Ignore"
								end
								if strBackVers:match(" Version (%d)%.") == strProgVers:match(" Version (%d)%.") then
									strButton = GUI_ModeDialogue(	"Minor Version Mismatch",
															"ERROR: "..lblProgram.Title.." installed does not match"..strBackVers.." Backup.\nIf possible, install a free update so the program version is compatible with the backup version."..strBeware,	-- V2.5
															strIgnore, "OK"	)
								else
									strButton = GUI_ModeDialogue(	"Major Version Mismatch",
															"ERROR: "..lblProgram.Title.." installed does not match"..strBackVers.." Backup.\nThe program settings for different Family Historian program major Versions are NOT compatible."..strBeware,	-- V2.5
															strIgnore, "OK"	)
								end
								if strButton == "OK" then break end								-- V2.5
							end
							loadMd5PathMap(strSourceDir)											-- Load the md5 to long file path map	-- V3.1
							local strOption = ""
							local strAdvice = "Do you want to Restore the Program Data Files or Registry Data Keys?"
							if #strTutorial > 0 then												-- V3.1
								strTutorial = ""
								IsTutorial = false
								local arrTutorFiles = general.GetFolderContents(strSourceDir..StrTutorials)
								if #arrTutorFiles > 0 then											-- Check if any Tutorial files -- V2.4 -- V3.2
									strTutorial = StrTutorFiles
									IsTutorial = true
									strAdvice = strAdvice:gsub("Restore the","Restore the Tutorial Files or")
								end
							end
							IntBackupMod, strModified = intTime(strVerFile,0)					-- Backup Folder modified date-time-stamp -- V3.2 -- V3.4
							strButton = GUI_ModeDialogue(	"Restore Data "..strModified,
																	"WARNING: Use 'Skip Operation' unless certain of the consequences of using Restore.\nHas a Backup been performed (by another PC or User or Project) for the current Program Settings?\n"..strAdvice,
																	strOption, strTutorial, strProgram, strRegistry, "Skip Operation"	)	-- V2.4 swap strTutorial and strProgram 
							if strButton == strTutorial then										-- Restore all Public Tutorial Files to Calico Pie folder
								strTutorial = ""
								local intFiles = 0
								if  general.FlgFolderExists(strSourceDir..StrTutorials)		-- Conditional restore -- V2.4
								and general.FlgFolderExists(strPublicPie) then
									showFolderPaths(strSourceDir..StrTutorials,strPublicPie..StrTutorials)
									intFiles = intCopyFiles(strSourceDir..StrTutorials,strPublicPie..StrTutorials,strProgName,"Restore",12)
								end
								if intFiles < 0 then break end
								if intFiles > 0 then
									intFileCount = intFileCount + intFiles
									lblStatus.Title = "Restore completed for "..intFiles.." tutorial files"
									lblFilename.Title = " "
									fhSleep(1000,200)
								end
							elseif strButton == strProgram then									-- Restore all Program Data files to Calico Pie folder and also Application Plugin Data
								strProgram = ""
								local intImages = intRestoreImageFiles(strSourceDir)
								showFolderPaths(strSourceDir..StrProg_Data,strTargetDir)		-- V2.0
								local intFiles = intCopyFiles(strSourceDir..StrProg_Data,strTargetDir,strProgName,"Restore",150)
								if intFiles < 0 then break end
								local intApps = 0
								if  general.FlgFolderExists(strSourceDir..StrUser_Data)		-- V2.4
								and general.FlgFolderExists(strApplicPie) then
									showFolderPaths(strSourceDir..StrUser_Data,strApplicPie)
									intApps = intCopyFiles(strSourceDir..StrUser_Data,strApplicPie,strProgName,"Restore",0)
								end
								if intApps < 0 then break end
								showFolderPaths(strSourceDir,strTargetDir)						-- V2.0
								intFiles = intFiles + intApps + intImages
								if intFiles > 0 then
									intFileCount = intFileCount + intFiles
									lblStatus.Title = "Restore completed for "..intFiles.." data files"
									lblFilename.Title = " "
									fhSleep(1000,200)
								end
							elseif strButton == strRegistry then									-- Import all Registry Data Keys for Program
								strRegistry = ""
								if doImportRegistry(strSourceDir,strProgName,strProgVers) < 0 then return iup.CLOSE end
								intFileCount = intFileCount + 1									-- V2.5
								lblFilename.Title = " "
								fhSleep(1000,200)
							end
						else
							strButton = "Skip Operation"
						end
					until strButton:match("Skip") or ( strProgram == "" and strTutorial == "" and ( strRegistry == "" or StrRegistry == "OFF" ) )
				else
					GUI_ModeDialogue(	"Backup Data Missing",
											"ERROR: Backup Folder has no "..lblProgram.Title.." backup data saved.", -- V3.0
											"OK"	)
				end
				if intFileCount > 0 then
					intFileTotal = intFileTotal + intFileCount
	 				intProgTotal = intProgTotal + 1
				end
			end -- per installed Program loop
			showCompletedStatus("Restore",intFileTotal,intProgTotal)							-- V2.5
		end
		setMainControlsActive("YES")
	end -- function btnRestore:action

	-- GUI Options Tab Functions --

	function btnFolder:action()																	-- Action for Choose Backup Folder button
		if setBackupFolderOK("Choose","") then
			lblBackData.Title = StrBackupDir:gsub("&","&&")
		end
	end -- function btnFolder:action

	function btnExplore:action()																	-- Action for Open Backup Folder button -- V2.6
		if setBackupFolderOK("Choose",StrBackupDir) then
			lblBackData.Title = StrBackupDir:gsub("&","&&")
			general.DoExecute(StrBackupDir)
		end
	end -- function btnExplore:action

	function btnDefault:action()																	-- Action for Restore Defaults button
		ResetDefaultSettings()
		setControls()
		iup_gui.ShowDialogue("Main")
		iup_gui.DefaultDialogue()																	-- V3.8
		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

	function tglDatedDir:action(intState)														-- Action for Use Dated Sub-folders toggle
		if intState == 0 then StrDatedDir = "OFF" end											-- 19 July 2013
		if intState == 1 then StrDatedDir = "ON" end
		if StrDatedDir == "OFF" then
			StrBackupDir = strRemoveDatedDir(StrBackupDir)
			lblBackData.Title = StrBackupDir
			iup_gui.RefreshDialogue("Main")
		end
		SaveSettings()																				-- Save sticky data settings
	end -- function tglDatedDir:action

	function tglDiagnose:action(intState)														-- Action for Enable Diagnostic Mode toggle
		if intState == 0 then StrDiagnose = "OFF" end
		if intState == 1 then StrDiagnose = "ON" end
		SaveSettings()																				-- Save sticky data settings
	end -- function tglDiagnose:action

	function tglRegistry:action(intState)														-- Action for Allow RESTORE Registry Data toggle
		if intState == 0 then StrRegistry = "OFF" end
		if intState == 1 then StrRegistry = "ON" end
		SaveSettings()																				-- Save sticky data settings
	end -- function tglRegistry:action

	function tglLaterVer:action(intState)														-- Action for Later Program Version checks toggle
		if intState == 0 then StrLaterVer = "OFF" end
		if intState == 1 then StrLaterVer = "ON" end
	end -- function tglLaterVer:action

	-- GUI Main Dialogue Global Functions --

	local intTabPosn = 0																			-- 0 means not chosen, otherwise tab posn number + 1	29 July 2013

	function tabControls:tabchangepos_cb(intNew,intOld)										-- Call back when Main tab position is changed
		setControls()
		intTabPosn = intNew + 1																		-- 29 July 2013
		if intNew == 0 and StrDatedDir == "ON" then												-- Opening Backup tab with dated sub-folders
			lblBackData.Title = strAppendDatedDir(StrBackupDir)								-- 19 July 2013
			iup_gui.RefreshDialogue("Main")
		end
		SaveSettings()																				-- Save sticky data settings
	end -- function tabControls:tabchangepos_cb

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

	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/backup-and-restore-family-historian-settings"
	local arrHelp = { "-backup-tab"; "-restore-tab"; "-options-tab"; }

	function btnGetHelp:action()																	-- Action for Help and Advice button
		local strPage = arrHelp[intTabPosn] or "" 												-- Tab dependent Help Page -- 31 July 2013
		doExecute( strHelp..strPage )
		fhSleep(2000,500)
		dialogMain.BringFront="YES"
	end -- function btnGetHelp:action

	function btnDestroy:action()																	-- Action for Close Plugin button
		return iup.CLOSE 
	end -- function btnDestroy:action

	function dialogMain:close_cb()																-- Call back when GUI window Close pressed
		return iup.CLOSE
	end -- function dialogMain:close_cb

	local timProgram = iup.timer { time=500; run="NO"; }										-- Allow main dialogue to start before running the script below

	function timProgram:action_cb()																-- Call back when Program Versions one-shot timer expires -- V2.0
		timProgram.run = "NO"
		if not doSetProgramVersions() then return iup.CLOSE end								-- Establish the installed Family Historian program Versions
		if not FlgFolderWrite(StrBackupDir) then												-- V2.9 -- V2.6
			if FlgFolderWrite(StrBackupKey) then													-- V2.9 -- V2.6 Fix to initial/synch settings between PC
				StrBackupDir = StrBackupKey
			else
				StrBackupDir = ""
			end
		end
		showFolderPaths(StrBackupDir,StrCalicoPie)												-- V2.6
		if StrDatedDir == "ON" then
			lblBackData.Title = strAppendDatedDir(StrBackupDir)								-- 19 July 2013 -- Moved here from after setMainControlsActive("NO") below -- V3.0
		end
		setMainControlsActive("YES")
	end -- function timProgram:action_cb

	-- GUI Main Dialogue Initialisation --

	setMainControlsActive("NO")																	-- V2.0
	timProgram.run = "YES"																			-- FH V7 IUP 3.28 timer may not run if started earlier -- V3.0
	iup_gui.ShowDialogue("Main",dialogMain,btnDestroy)

end -- function GUI_MainDialogue

-- Main body of Plugin script starts here --

	if fhGetContextInfo("CI_APP_MODE") == "Project Mode" then								-- V3.7

		fhInitialise(5,0,0,"save_recommended")													-- V2.0

		PresetGlobalData()																			-- Preset global data definitions

		ResetDefaultSettings()																		-- Preset default sticky settings

		LoadSettings()																				-- Load sticky data settings

		StrInitialMessage = [[
		Ensure you digest the Help && Advice before using the Plugin, especially regarding RESTORE operations. 

		This Plugin can change your Windows Registry (low-level PC settings) if you use its RESTORE option. 
		However, this 'RESTORE Registry Data' feature is initially disabled by default on the Options tab. 
		Nevertheless, this plugin only makes similar changes as Family Historian does each time it runs. 
		If in doubt, backup the Windows Registry before using the 'RESTORE Registry Data' feature. 

		If you have any questions about this Plugin, or encounter any problems as a consequence of using it, 
		contact the Plugin's author, Mike Tate, via the FHUG at www.fhug.org.uk or on the FH Mailing List. 

		Please do NOT contact Calico Pie, who are not responsible for this Plugin.          Thank you.
		]]

		if iup_gui.WarnDialogue({" ~  IMPORTANT  PLEASE  READ  ~ ";iup_gui.Risk;},{StrInitialMessage;iup_gui.Risk;},{"OK  Continue";iup_gui.Safe;},{"Cancel  Plugin";iup_gui.Risk;}) == 1 then
			GUI_MainDialogue()
		end

		SaveSettings()																				-- Save sticky data settings
	else
		iup_gui.WarnDialogue({" Plugin not in Project Mode ";iup_gui.Risk;},{" This plugin is only designed to be run from within a Project and not a standalone GEDCOM. ";iup_gui.Risk;})
	end
--[=[
Windows Registry Management Notes

1)	Virtualisation for HKLM on Windows 7 & Vista (but not XP) allows Standard User to 'write' to HKLM keys.
	See http://msdn.microsoft.com/en-us/library/windows/desktop/aa965884%28v=vs.85%29.aspx

2)	Migrating 64/32 bit using %WinDir%\SysWOW64\reg.exe to avoid "HKLM\Software\WOW6432Node\..." problem.
	See http://csi-windows.com/blog/all/27-csi-news-general/266-scripting-migrating-and-managing-registry-data-in-64-bit-windows
	In following command %WinDir%\SysWOW64\REG.EXE actually exports from "HKLM\Software\WOW6432Node\Calico Pie\Family Historian" but hides \WOW6432Node\
	IF EXIST	%WinDir%\SysWOW64\REG.EXE (
				%WinDir%\SysWOW64\REG.EXE EXPORT "HKLM\Software\Calico Pie\..." "HKLM.reg"	/Y
	) ELSE (
											REG	 EXPORT "HKLM\Software\Calico Pie\..." "HKLM.reg"	/Y
	)

3)	Actual testing suggests none of the above is necessary. Plain REG works OK and \WOW6432Node\ disregarded. The /Y option is not allowed on XP.
	The HKEY_LOCAL_MACHINE\SOFTWARE\[WOW6432Node]\Calico Pie\Family Historian... keys grant Everyone permission for Full Control.
	This explains why any user account can write to those keys. 
]=]

--[=[
@V3.8:				Library v4.1; Centre windows on FH window; Check for Updates button; Cater for 64-bit FH V8; setToolTip(iupItem) removed; Include the \Map\Overlays\ folders;
@V3.7:				Library v3.7; Project Mode only; Crossover/Wine variants removed; Updated GetFolderContents(...) for WINE; Dated sub-folders use time too; Replace io.lines(...);
@V3.6:				Replace general.DirTree(...) with general.GetFolderContents(...) in intCopyFiles(...); Update Crossover/Wine Reg Keys;
@V3.5:				Fix for StrProcArch64; Add intTime(...) luacom error protection and repeat if date-time is 0; Report missing C:\ProgramData\...\Plugin Data\ folder in doSetProgramVersions(); Library V3.3 with better CheckVersionInStore();
@V3.3&4:			Cater for all date-time formats in intTime(…) especially for English UK, USA & European countries;
@V3.2:				Insert WOW6432Node into HKLM\SOFTWARE\ path to support Windows PowerShell as well as Cmd prompt unless HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\PROCESSOR_ARCHITECTURE = 'x86';
@V3.2:				Library Version 3.2 with FSO to support Unicode file paths; Delete unwanted FH V6 md5 library in FH V7; general.DetectOldModules(); Update Crossover/Wine Reg Keys;
@V3.1:				iup_gui.VersionInStore("Backup…Settings"); Updated Registry keys; Protected mode FileSystemObject:CopyFile( ); Excessive long paths > 250; Garbage memory manage;
@V3.0:				FH V7 Lua 3.5 IUP 3.28; Revised GUI labels, and Help & Advice; FH closure updates …\Fact Types\Standard\GroupIndex.fhdata, so need special handling in RESTORE;
@V2.9:				Latest library V2.9; iup_gui.Balloon = "NO" for PlayOnLinux/Mac; If 'Family Historian' folder chosen, advise move up to parent; Preserve folder Date modified;
@V2.8:				Fix standalone Gedcom 'Cannot Make Folder' and GetRegKey() IE Shell problems in library, and use Program Data folder path to detect Crossover/Wine.
@V2.7:				Add global Backup folder sticky setting to act as default for local Backup folder.
@V2.6:				Relaxed check uses integer Version to allow digits > 9, cater for FH Beta mode by adjusting Registry.keys file HKCU & HKLM keys plus Open Backup Folder button, fix initial/synch settings across PC.
@V2.5:				Update for FH V6 or later, Crossover full support, Diagnose mode retains temp files, and new LaterVer toggle to relax Version check; FH closure updates …\Roaming\…\Preferences\prefs.dat colours, so need special handling in RESTORE;
@V2.4:				runBatchFileOK() & doSetProgramVersion() & doExport/ImportRegistry() handle Unicode in Registry, plus Crossover experiments and GetRegKey(), and bug fixes for FH V6.1.4.
@V2.3:				Avoid counting all files ~ line 3174, and exclude \Map\Cache via revised general.DirTree() ~ line 3187 to avoid '(Not Responding)' message.
@V2.2:				FH V5 & V6 IUP 3.11.2 HelpDialog conditional ExpandChildren="YES/NO", RefreshDialogue uses NaturalSize, Load/SaveFolder("Backup",StrBackupDir),
@V2.2:				allow 0 Tutorial Files in V6, exclude \Map\Cache\ files, better ERROR message for Too Few Data Files, Migrate Beta option, XP setToolTip(iupItem) function.
@V2.1:				Revised doSetProgramVersions() to cater for hex digits in versions, and added runBatchFileOK() isModal setting for "CHOICE" mode.
@V2.1:				PROBLEM with Options > Restore Defaults > Choose Backup Folder > Cancel > faulty popup window - fixed in DialogueAttributes library function 20 Jul 2014
@V2.1:				Revised iup_gui module, revised Windows Registry access using modal WScript.Shell that needs no timeout and works with Crossover & Wine.
@V2.1:				Revised iup_gui module fhGetPluginDataFileName() for any FH Version, inhibit Minimize & Maximize icons in popups, add BalloonToggle().
@V2.0:				VersionInStore() only reports Internet inaccessible once while Plugins in regular use.
@V2.0:				Include os.getenv("PUBLIC")/os.getenv("ALLUSERSPROFILE")\Calico Pie\Family Historian\Tutorial Files\ in Backup & Restore.
@V2.0:				Include os.getenv("APPDATA")\Calico Pie\Family Historian\Plugin Data\ in Backup & Restore, new fh library modules, general code updates.
@V2.0:				Change the Batch jobs to use mostly local files, and modified completion method, to improve NAS device support.
@V2.0:				Inhibit main parent window while Backup or Restore is running, and report clearly when the operation has failed.
@V1.9:				Add NewGUI library, revised Version History help - must delete FHUG '...help:backup_and_restore_family_historian_settings:history' page.
@V1.8:				Update to FlgFolderExists(), cater for \\NAME\ in place of C:\, adjust runBatchFileOK() timeout counter, add Diagnose mode,
					optionally use date stamped backup sub-folders to allow multiple backups, Version History help, and new plain string snippets.
@V1.7:				Allow 1-hour DST (GMT/BST) offsets, fix StrVersionInStore, add StrWhite GUI background, and minor revisions.
@V1.6:				New disallow RESTORE Registry Data Keys option, migrate FH 5.0 Beta option removed,
					allow RESTORE to create folders & report failures, omit file paths from HKLM Registry Keys,
					introductory IMPORTANT PLEASE READ message, convert filename "&" to "&&" in GUI titles,
					function doCopyFile 'Restore Old File' WARNING adapted for 'Backup Old File' too,
					delete redundant image file backups.
@V1.5:				Migrate adapted for disabled FH 5.0 Beta.
@V1.4:				Added FHUG Help and Advice pages.
@V1.3:				Handles 1 hour time difference on Migrate Beta to Full V5, hides Restore Data buttons as they are used.
@V1.2:				REG QUERY /V /S reduced to REG QUERY /S for XP, Batch script timeout longer, Copy File tries all Write modes,
					also saves duplicate external images only once, and skips those in Program Data folders.
@V1.1:				Migrate FH 5.0 Beta Backup to FH V5.0, backup external image files, plus other minor updates. 
@V1.0:				First version.
]=]

Source: Backup-and-Restore-Family-Historian-Settings-11.fh_lua