Change Any Fact Tag.fh_lua

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

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

--[[
@Title:			aa Library Functions Preamble
@Author:			Mike Tate
@Version:			4.0
@LastUpdated:		15 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 = "
" -- Markup language break tag default local br_Lua = "
" -- 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" -- """ in Markup see setURIEncodings() and setMarkupEncodings() below tblCodePage["#"] = "%23" -- Reserved character tblCodePage["$"] = "%24" -- Reserved character tblCodePage["%"] = "%25" -- Must be encoded tblCodePage["&"] = "%26" -- Reserved character -- "&" in Markup see setURIEncodings() and setMarkupEncodings() below tblCodePage["'"] = "%27" -- Reserved character -- "'" 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" -- "<" in Markup see setURIEncodings() and setMarkupEncodings() below tblCodePage["="] = "%3D" -- Reserved character tblCodePage[">"] = "%3E" -- ">" 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) -- "€" 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) -- " " No Break Space tblCodePage["¡"] = string.char(0xC2,0xA1) -- "¡" tblCodePage["¢"] = string.char(0xC2,0xA2) -- "¢" tblCodePage["£"] = string.char(0xC2,0xA3) -- "£" tblCodePage["¤"] = string.char(0xC2,0xA4) -- "¤" tblCodePage["¥"] = string.char(0xC2,0xA5) -- "¥" 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) -- "­" 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 "
" or "
" or "
" or "
" is better tblCodePage["\f"] = br_Tag -- FF tblCodePage["\r"] = br_Tag -- CR tblCodePage['"'] = """ tblCodePage["&"] = "&" tblCodePage["'"] = "'" tblCodePage["<"] = "<" tblCodePage[">"] = ">" 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
&
&
&
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
&
&
&
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
&
&
&
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
&
&
&
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"]=" " -- " " No Break Space tblASCII["¡"] = "!" -- "¡" tblASCII["¢"] = "=c" -- "¢" tblASCII["£"] = "=L" -- "£" tblASCII["¤"] = "=$" -- "¤" tblASCII["¥"] = "=Y" -- "¥" tblASCII["¦"] = "|" tblASCII["§"] = "=SS" tblASCII["¨"] = "\"" tblASCII["©"] = "(C)" tblASCII["ª"] = "a" tblASCII["«"] = "<<" tblASCII["¬"] = "-" tblASCII["­"] = "-" -- "­" 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 "
" or "
" or "
" or "
" br_New = "
" 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.5 @LastUpdated: 15 Feb 2026 @Description: Graphical User Interface Library Module @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 minimise this window size -- V4.4 tblName.Raster= iupDialog.MinSize 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 and fh.SetWindowCoord(tblName) then tblName.Raster=self.RasterSize end end -- V3.5 -- V4.4 fh.RefreshDialogue(strName) -- Refresh to set Natural Size as Minimum Size if strName == "Main" then if fhGetAppVersion() > 6 then -- Main window centres on FH parent -- V4.4 iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND")) end else local tblMain = tblNameFor("Main") -- Others popup centrally in Main -- V4.4 local iupMain = tblMain.Dialog if iupMain then -- Centre based on size of windows -- V1.4 local arrName = stringx.splitnumbers(tblName.Raster or "0x0") local arrMain = stringx.splitnumbers(tblMain.Raster or arrName[1].."x"..arrName[2]) tblName.CoordX = iupMain.X + math.floor( (arrMain[1] - arrName[1]) / 2 ) tblName.CoordY = iupMain.Y + math.floor( (arrMain[2] - arrName[2]) / 2 ) elseif fhGetAppVersion() > 6 then -- This window centres on FH parent -- V4.4 iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND")) end end if strFrame:match("map") then -- Only dialogue mapping is required iupDialog:map() tblName.Frame = strFrame:gsub("%s-%a-map%a*[%s%p]*","") -- Remove "map" from frame mode ready for subsequent call return end if iup.MainLoopLevel() == 0 -- Called from outside Main GUI, so must use showxy() and not popup() or strFrame:match("dialog") or strFrame:match("sho") then -- Use showxy() to dispay dialogue window for "showxy" or "dialog" mode iupDialog:showxy(tblName.CoordX,tblName.CoordY) if not strFrame:match("dialog") -- Inhibit MainLoop if "dialog" mode -- V4.1 and iup.MainLoopLevel() == 0 then iup.MainLoop() end else iupDialog:popup(tblName.CoordX,tblName.CoordY) -- Use popup() to display dialogue window for "popup" or "keep" modes fhSleep(200,150) -- Sometimes needed to prevent MainLoop() closure! -- V3.9 end if not strFrame:match("dialog") and strFrame:match("pop") then tblName.Dialog = nil -- When popup closed, clear key parameters, but not for "keep" mode tblName.Raster = nil tblName.CoordX = nil -- iup.CENTERPARENT tblName.CoordY = nil -- iup.CENTERPARENT elseif tblName.CoordX ~= iup.CENTERPARENT -- V4.4 and tblName.CoordY ~= iup.CENTERPARENT then fh.SetWindowCoord(tblName) -- Set Window coordinate pixel values -- V3.5 end end end -- function ShowDialogue function fh.DestroyDialogue(strName) -- Destroy existing dialogue local tblName = tblNameFor(strName) if tblName then local iupDialog = tblName.Dialog if type(iupDialog) == "userdata" then iupDialog:destroy() tblName.Dialog = nil -- Prevent future misuse of handle -- 22 Jul 2014 end end end -- function DestroyDialogue local function strDialogueArgs(strArgA,strArgB,comp) -- Compare two argument pairs and return matching pair local tblArgA = stringx.splitnumbers(strArgA) local tblArgB = stringx.splitnumbers(strArgB) local strArgX = tostring(comp(tblArgA[1] or 100,tblArgB[1] or 100)) local strArgY = tostring(comp(tblArgA[2] or 100,tblArgB[2] or 100)) return strArgX.."x"..strArgY end -- local function strDialogueArgs function fh.RefreshDialogue(strName) -- Refresh dialogue window size after Font change, etc local tblName = tblNameFor(strName) local iupDialog = tblName.Dialog -- Retrieve the dialogue handle if type(iupDialog) == "userdata" then iupDialog.Size = iup.NULL iupDialog.MinSize = iup.NULL -- V3.1 iup.Refresh(iupDialog) -- Refresh window to Natural Size and set as Minimum Size if not iupDialog.RasterSize then iupDialog:map() iup.Refresh(iupDialog) end local strSize = iupDialog.NaturalSize or iupDialog.RasterSize -- IUP 3.5 NaturalSize = nil, IUP 3.11 needs NaturalSize -- V3.1 iupDialog.MinSize = strDialogueArgs(iupDialog.MaxSize,strSize,math.min) -- Set Minimum Size to smaller of Maximm Size or Natural/Raster Size -- V3.1 iupDialog.RasterSize = strDialogueArgs(tblName.Raster,strSize,math.max) -- Set Current Size to larger of Current Size or Natural/Raster Size -- V3.1 iup.Refresh(iupDialog) tblName.Raster = iupDialog.RasterSize if iupDialog.Visible == "YES" then -- Ensure visible dialogue origin is on screen tblName.CoordX = math.max(tblName.CoordX,10) tblName.CoordY = math.max(tblName.CoordY,10) -- Set both coordinates to larger of current value or 10 pixels if iupDialog.Modal then -- V3.8 if iupDialog.Modal == "NO" then iupDialog.ZOrder = "BOTTOM" -- Ensure dialogue is subservient to any popup iupDialog:showxy(tblName.CoordX,tblName.CoordY) -- Use showxy() to reposition main window else iupDialog:popup(tblName.CoordX,tblName.CoordY) -- Use popup() to reposition modal window end end else iupDialog.BringFront="YES" end end end -- function RefreshDialogue function fh.AssignAttributes(tblControls) -- Assign the attributes of all controls supplied local anyFunction = nil for iupName, tblAttr in pairs ( tblControls or {} ) do if type(iupName) == "userdata" and type(tblAttr) == "table" then -- Loop through each iup control local intSkip = 0 -- Skip counter for attributes same for all controls for intAttr, anyName in ipairs ( tblControls[1] or {} ) do -- Loop through each iup attribute local strName = nil local strAttr = nil local strType = type(anyName) if strType == "string" then -- Attribute is different for each control in tblControls strName = anyName strAttr = tblAttr[intAttr-intSkip] elseif strType == "table" then -- Attribute is same for all controls as per tblControls[1] intSkip = intSkip + 1 strName = anyName[1] strAttr = anyName[2] elseif strType == "function" then intSkip = intSkip + 1 anyFunction = anyName break end if type(strName) == "string" and ( type(strAttr) == "string" or type(strAttr) == "function" ) then local anyRawGet = rawget(fh,strAttr) -- Use rawget() to stop require("pl.strict") complaining if type(anyRawGet) == "string" then strAttr = anyRawGet -- Use internal module attribute such as Head or FontBody elseif type(iupName[strName]) == "string" and type(strAttr) == "function" then -- Allow string attributes to invoke a function -- V3.7 strAttr = strAttr() end iupName[strName] = strAttr -- Assign attribute to control end end end end if anyFunction then anyFunction() end -- Perform any control assignment function end -- function AssignAttributes -- Font Dialogue Attributes and Functions -- fh.FontBody = iup.GetGlobal("DEFAULTFONT") -- Set default font for Body and Head text fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold ") function fh.FontDialogue(tblAttr,strName) -- GUI Font Face & Style Dialogue tblAttr = tblAttr or {} strName = strName or "Main" local isFontChosen = false local btnFontHead = iup.button { Title="Choose Headings Font and default Colour"; } local btnFontBody = iup.button { Title="Choose Body text Font and default Colour"; } local btnCol_Safe = iup.button { Title=" Safe Colour "; } local btnCol_Warn = iup.button { Title=" Warning Colour "; } local btnCol_Risk = iup.button { Title=" Risky Colour "; } local btnDefault = iup.button { Title=" Default Fonts "; } local btnMinimum = iup.button { Title=" Minimum Size "; } local btnDestroy = iup.button { Title=" Close Dialogue "; } local frmSetFonts = iup.frame { Title=" Set Window Fonts & Colours "; iup.vbox { Alignment="ACENTER"; Margin=fh.Margin; Homogeneous="YES"; btnFontHead; btnFontBody; iup.hbox { btnCol_Safe; btnCol_Warn; btnCol_Risk; Homogeneous="YES"; }; iup.hbox { btnDefault ; btnMinimum ; btnDestroy ; Homogeneous="YES"; }; } -- iup.vbox end } -- iup.frame end -- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button local dialogFont = iup.dialog { Title=" Set Window Fonts & Colours "; Gap=fh.Gap; Margin=fh.Border; frmSetFonts; } local tblButtons = { } local function setDialogues() -- Refresh the Main dialogue -- V4.4 fh.AssignAttributes(tblAttr) -- Assign parent dialogue attributes fh.RefreshDialogue(strName) -- Refresh parent window size & position and bring infront of other window fh.RefreshDialogue("Font") -- Refresh Font window size & position and bring infront of parent window end -- local function setDialogues local function getFont(strColor) -- Set font button function local strTitle = " Choose font style & default colour for "..strColor:gsub("Head","Heading").." text " local strValue = "Font"..strColor -- The font codes below are not recognised by iupFontDlg and result in empty font face! local strFont = rawget(fh,strValue):gsub(" Black,",","):gsub(" Light, Bold",","):gsub(" Extra Bold,",","):gsub(" Semibold,",",") local iupFontDlg = iup.fontdlg { Title=strTitle; Color=rawget(fh,strColor); Value=strFont; } iupFontDlg:popup() -- Popup predefined font dialogue if iupFontDlg.Status == "1" then if iupFontDlg.Value:match("^,") then -- Font face missing so revert to original font iupFontDlg.Value = rawget(fh,strValue) end fh[strColor] = iupFontDlg.Color -- Set Head or Body color attribute fh[strValue] = iupFontDlg.Value -- Set FontHead or FontBody font style fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes setDialogues() isFontChosen = true end end -- local function getFont local function getColor(strColor) -- Set colour button function local strTitle = " Choose colour for "..strColor:gsub("Warn","Warning"):gsub("Risk","Risky").." button & message text " local iupColorDlg = iup.colordlg { Title=strTitle; Value=rawget(fh,strColor); ShowColorTable="YES"; } iupColorDlg.DialogFrame="YES" iupColorDlg:popup() -- Popup predefined color dialogue fixed size window if iupColorDlg.Status == "1" then fh[strColor] = iupColorDlg.Value -- Set Safe or Warn or Risk color attribute fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes setDialogues() isFontChosen = true end end -- local function getColor local function setDefault() -- Action for Default Fonts button fh.Safe = fh.Green fh.Warn = fh.Magenta fh.Risk = fh.Red -- Set default colours fh.Body = fh.Black fh.Head = fh.Black fh.FontBody = iup.GetGlobal("DEFAULTFONT") -- Set default fonts for Body and Head text fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold") fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes setDialogues() isFontChosen = true end -- local function setDefault local function setMinimum() -- Action for Minimum Size button local tblName = tblNameFor(strName) local iupDialog = tblName.Dialog -- Retrieve the parent dialogue handle if type(iupDialog) == "userdata" then tblName.Raster = "10x10" -- Refresh parent window to Minimum Size & adjust position fh.RefreshDialogue(strName) end local tblFont = tblNameFor("Font") tblFont.Raster = "10x10" -- Refresh Font window to Minimum Size & adjust position fh.RefreshDialogue("Font") end -- local function setMinimum tblButtons = { { "Font" ; "FgColor" ; "Tip" ; "action" ; {"TipBalloon";"Balloon";} ; {"Expand";"YES";} ; }; [btnFontHead] = { "FontHead"; "Head"; "Choose the Heading text Font Face, Style, Size, Effects, and default Colour"; function() getFont("Head") end; }; [btnFontBody] = { "FontBody"; "Body"; "Choose the Body text Font Face, Style, Size, Effects, and default Colour" ; function() getFont("Body") end; }; [btnCol_Safe] = { "FontBody"; "Safe"; "Choose the colour for Safe operations" ; function() getColor("Safe") end; }; [btnCol_Warn] = { "FontBody"; "Warn"; "Choose the colour for Warning operations"; function() getColor("Warn") end; }; [btnCol_Risk] = { "FontBody"; "Risk"; "Choose the colour for Risky operations" ; function() getColor("Risk") end; }; [btnDefault ] = { "FontBody"; "Safe"; "Restore default Fonts and Colours"; function() setDefault() end; }; [btnMinimum ] = { "FontBody"; "Safe"; "Reduce window to its minimum size"; function() setMinimum() end; }; [btnDestroy ] = { "FontBody"; "Risk"; "Close this dialogue "; function() return iup.CLOSE end; }; [frmSetFonts] = { "FontHead"; "Head"; }; } fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep normal") -- Popup the Set Window Fonts dialogue: "keep normal" : vary size & posn, and remember size & posn -- fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup normal") -- Popup the Set Window Fonts dialogue: "popup normal" : vary size & posn, but redisplayed centred -- fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep") -- Popup the Set Window Fonts dialogue: "keep" : fixed size, vary posn, and only remember posn -- fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup") -- Popup the Set Window Fonts dialogue: "popup": fixed size, vary posn, but redisplayed centred dialogFont:destroy() return isFontChosen end -- function FontDialogue local function anyMemoControl(anyName,fgColor) -- Compose any control Title and FgColor local strName = tostring(anyName) -- anyName may be a string, and fgColor is default FgColor local tipText = nil if type(anyName) == "table" then -- anyName may be a table = { Title string ; FgColor string ; ToolTip string (optional); } strName = anyName[1] fgColor = anyName[2]:match("%d* %d* %d*") or fgColor tipText = anyName[3] end return strName, fgColor, tipText end -- local function anyMemoControl local function anyMemoDialogue(strHead,anyHead,strMemo,anyMemo,...) -- Display framed memo dialogue with buttons local arg = {...} -- Fix for Lua 5.2+ local intButt = 0 -- Returned value if "X Close" button is used local tblButt = { [0]="X Close"; } -- Button names lookup table local strHead, fgcHead, tipHead = anyMemoControl(anyHead or "",strHead) local strMemo, fgcMemo, tipMemo = anyMemoControl(anyMemo or "",strMemo) -- Create the GUI labels and buttons local lblMemo = iup.label { Title=strMemo; FgColor=fgcMemo; Tip=tipMemo; TipBalloon=fh.Balloon; Alignment="ACENTER"; Padding=fh.Margin; Expand="YES"; WordWrap="YES"; } local lblLine = iup.label { Separator="HORIZONTAL"; } local iupHbox = iup.hbox { Homogeneous="YES"; } local btnButt = iup.button { } local strTop = "YES" -- Make dialogue TopMost -- V3.6 local strMode = "popup" if arg[1] == "Keep Dialogue" then -- Keep dialogue open for a progress message strMode = "keep dialogue" lblLine = iup.label { } if not arg[2] then strTop = "NO" end -- User chooses TopMost -- V3.6 else if #arg == 0 then arg[1] = "OK" end -- If no buttons listed then default to an "OK" button for intArg, anyButt in ipairs(arg) do local strButt, fgcButt, tipButt = anyMemoControl(anyButt,fh.Safe) tblButt[intArg] = strButt btnButt = iup.button { Title=strButt; FgColor=fgcButt; Tip=tipButt; TipBalloon=fh.Balloon; Expand="NO"; MinSize="80"; Padding=fh.Margin; action=function() intButt=intArg return iup.CLOSE end; } iup.Append( iupHbox, btnButt ) end end -- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button local iupMemo = iup.dialog { Title=fh.Plugin..fh.Version..strHead; TopMost=strTop; -- TopMost added -- V3.6 iup.vbox { Alignment="ACENTER"; Gap=fh.Gap; Margin=fh.Margin; iup.frame { Title=strHead; FgColor=fgcHead; Font=fh.FontHead; iup.vbox { Alignment="ACENTER"; Font=fh.FontBody; lblMemo; lblLine; iupHbox; }; }; }; } fh.ShowDialogue("Memo",iupMemo,btnButt,strMode) -- Show popup Memo dialogue window with righthand button in focus (if any) if strMode == "keep dialogue" then return lblMemo, iupMemo end -- Return label & dialogue controls so message can be changed and dialogue destroyed iupMemo:destroy() return intButt, tblButt[intButt] -- Return button number & title that was pressed end -- local function anyMemoDialogue function fh.MemoDialogue(anyMemo,...) -- Multi-Button GUI like iup.Alarm and fhMessageBox, with "Memo" in frame return anyMemoDialogue(fh.Head,"Memo",fh.Body,anyMemo,...) end -- function MemoDialogue function fh.WarnDialogue(anyHead,anyMemo,...) -- Multi-Button GUI like iup.Alarm and fhMessageBox, with heading in frame return anyMemoDialogue(fh.Warn,anyHead,fh.Warn,anyMemo,...) end -- function WarnDialogue function fh.GetRegKey(strKey) -- Read Windows Registry Key Value local luaShell = luacom.CreateObject("WScript.Shell") local anyValue = nil if pcall( function() anyValue = luaShell:RegRead(strKey) end ) then return anyValue -- Return Key Value if found end return nil end -- function GetRegKey function fh.PutRegKey(strKey,anyValue,strType) -- Write Windows Registry Key Value local luaShell = luacom.CreateObject("WScript.Shell") local strAns = nil if pcall( function() strAns = luaShell:RegWrite(strKey,anyValue,strType) end ) then return true end return nil end -- function PutRegKey local function httpRequest(strRequest) -- Luacom http request protected by pcall() below local http = luacom.CreateObject("winhttp.winhttprequest.5.1") http:Open("GET",strRequest,false) http:Send() return http.Responsebody end -- local function httpRequest function fh.VersionInStore(strPlugin) -- Obtain the Version in Plugin Store by Name only -- V4.5 local strVersion = "0" if strPlugin then local strFile = fh.MachinePath.."\\VersionInStore "..strPlugin..".dat" general.DeleteFile(strFile) local lblMemo, iupMemo = fh.MemoDialogue("Checking for updated version in the Family Historian 'Plugin Store'.","Keep Dialogue") local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..strPlugin local isOK, strReturn = pcall(httpRequest,strRequest) iupMemo:destroy() if not isOK then -- Problem with Internet access fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ","MB_OK","MB_ICONEXCLAMATION") elseif strReturn ~= nil then strVersion = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits end end return strVersion or "0" end -- function VersionInStore local function intVersion(strVersion) -- Convert version string to comparable integer local intVersion = 0 local arrNumbers = {} strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end) -- V4.1 for i=1,5 do intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0) end return intVersion end -- local function intVersion function fh.CheckVersionInStore() -- Check if later Version available in Plugin Store -- V4.5 local strNewVer = fh.VersionInStore(fh.Plugin:gsub(" %- .*","")) local strOldVer = fh.Version if intVersion(strNewVer) > intVersion(strOldVer:match("%D*([%d%.]*)")) then fh.MemoDialogue("Later Version "..strNewVer.." of this Plugin is available from the 'Plugin Store'.") else fh.MemoDialogue("No later Version of this Plugin is available from the 'Plugin Store'.") end end -- function CheckVersionInStore function fh.PluginDataScope(strScope) -- Set default Plugin Data scope to per-Project, or per-User, or per-Machine strScope = tostring(strScope):lower() if strScope:match("mach") then -- Per-Machine strDefaultScope = "Machine" elseif strScope:match("user") then -- Per-User strDefaultScope = "User" end -- Per-Project is default end -- function PluginDataScope local function getPluginDataFileName(strScope) -- Get plugin data filename for chosen scope local isOK, strDataFile = pcall(fhGetPluginDataFileName,strScope) if not isOK then strDataFile = fhGetPluginDataFileName() end -- Before V5.0.8 parameter is disallowed and default = CURRENT_PROJECT return strDataFile end -- local function getPluginDataFileName local function getDataFiles(strScope) -- Compose the Plugin Data file & path & root names local strPluginName = fh.Plugin local strPluginPlain = stringx.plain(strPluginName) local strDataRoot = "" -- Plugin data file root name -- V4.2 local strDataPath = "" -- Plugin data folder path name local strDataFile = getPluginDataFileName(strScope) -- Allow plugins with variant filenames to use same plugin data files strDataFile = strDataFile:gsub("\\"..strPluginPlain:gsub(" ","_"):lower(),"\\"..strPluginName) strDataFile = strDataFile:gsub("\\"..strPluginPlain..".+%.[D,d][A,a][T,t]$","\\"..strPluginName..".dat") if #strDataFile > 0 then -- Standalone GEDCOM path is "" strDataPath = strDataFile:gsub("\\"..strPluginPlain.."%.[D,d][A,a][T,t]$","") strDataRoot = strDataPath.."\\"..strPluginName general.MakeFolder(strDataPath) -- V3.4 end return strDataFile, strDataPath, strDataRoot end -- local function getDataFiles function fh.Initialise(strVersion,strPlugin) -- Initialise the GUI module with optional Version & Plugin name local strAppData = fhGetContextInfo("CI_APP_DATA_FOLDER") fh.Plugin = fhGetContextInfo("CI_PLUGIN_NAME") -- Plugin Name from file fh.Version = strVersion or " " -- Plugin Version if fh.Version == " " then local strTitle = "\n@Title is missing" local strAuthor = "\n@Author is missing" local strVersion = "\n@Version is missing" local strPlugin = strAppData.."\\Plugins\\"..fh.Plugin..".fh_lua" if general.FlgFileExists(strPlugin) then for strLine in io.lines(strPlugin) do -- Read each line from the Plugin file strPlugin = strLine:match("^@Title:[\t-\r ]*(.*)") if strPlugin then strPlugin = strPlugin:gsub("&&","&") --? if fh.Plugin:match("^"..strPlugin:gsub("(%W)","%%%1")) then if fh.Plugin:match("^"..stringx.plain(strPlugin)) then fh.Plugin = strPlugin -- Prefer Title to Filename if it matches strTitle = nil else strTitle = "\n@Title differs from Filename" -- Report abnormality end end if strLine:match("^@Author:%s*(.*)") then -- Check @Author exists strAuthor = nil end fh.Version = strLine:gsub("^@Version:%D*([%d%.]*)%D*"," %1 ") if fh.Version ~= strLine then -- Obtain the @Version from Plugin file strVersion = nil break end end if strTitle or strAuthor or strVersion then -- Report any header abnormalities fhMessageBox("\nScript Header: "..fh.Plugin..(strTitle or "")..(strAuthor or "")..(strVersion or ""),"MB_OK","MB_ICONEXCLAMATION") end else fhMessageBox("\nPlugin has not been saved!","MB_OK","MB_ICONEXCLAMATION") end end fh.History = fh.Version -- Version History fh.Plugin = strPlugin or fh.Plugin -- Plugin Name from argument or default from file fh.DefaultDialogue() -- Default "Font","Main" dialogues fh.MachineFile,fh.MachinePath,fh.MachineRoot = getDataFiles("LOCAL_MACHINE") -- Plugin data names per machine fh.PerUserFile,fh.PerUserPath,fh.PerUserRoot = getDataFiles("CURRENT_USER") -- Plugin data names per user fh.ProjectFile,fh.ProjectPath,fh.ProjectRoot = getDataFiles("CURRENT_PROJECT") -- Plugin data names per project fh.FhDataPath = fhGetContextInfo("CI_PROJECT_DATA_FOLDER") -- Paths used by Load/SaveFolder for relative folders -- V4.0 fh.PublicPath = fhGetContextInfo("CI_PROJECT_PUBLIC_FOLDER") -- Public data folder path name -- V4.0 if fh.FhDataPath == "" then fh.FhDataPath = fh.ProjectPath:gsub("\\Plugin Data$","") end if fh.PublicPath == "" then fh.PublicPath = fh.ProjectPath fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Plugin Data$","%1") else general.MakeFolder(fh.PublicPath) -- V3.4 fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Public$","%1") end fh.CalicoPie = strAppData:gsub("\\Calico Pie\\.*","\\Calico Pie") -- Program Data Calico Pie path name fh.ComputerName = os.getenv("COMPUTERNAME") -- Local PC Computer Name end -- function Initialise fh.Initialise() -- Initialise module with default values return fh end -- local function iup_gui_v3 local iup_gui = iup_gui_v3() -- To access FH IUP GUI build module -- Preset Global Data Definitions -- function PresetGlobalData() -- Compose Date & Time Suffix for Log files -- local function strDateTime() local tblDate = {} tblDate = os.date("*t") return " - "..string.format("%04d",tblDate["year"]).."-"..string.format("%02d",tblDate["month"]).."-"..string.format("%02d",tblDate["day"]).." "..string.format("%02d",tblDate["hour"])..string.format("%02d",tblDate["min"])..string.format("%02d",tblDate["sec"]) end -- local function strDateTime local function strToUTF8(strFileName) if string.encoding() == "ANSI" then return strFileName end return fhConvertANSItoUTF8(strFileName) end -- local function strToUTF8 -- GUI Global Constants iup_gui.Gap = "4" iup_gui.Margin = "3x1" iup_gui.Balloon = "NO" -- Needed for PlayOnLinux/Mac -- V3.1 -- Tip Balloon popup 13 Nov 2013 iup_gui.SetUtf8Mode() StrPluginTitle = iup_gui.Plugin -- UTF-8 file details -- V3.7 StrProjectPath = iup_gui.ProjectPath StrLogFileName = StrPluginTitle..strDateTime()..".log" StrLogFilePath = StrProjectPath.."\\"..StrLogFileName IntMaxNameLen = 22 -- Length at which Button label Tag Names are truncated ArrLogFileText = {} StrLogNeedTitle = "Log Edits to File" -- Radio button titles StrPatternTitle = "Pattern Filters" StrConfirmTitle = "Confirm all Edits" StrWarningTitle = "Show all Warnings" StrOldData = "" -- Description of Source Tag e.g. Birth, Occupation, etc StrNewData = "" -- Description of Target Tag -- Statistics for Tags processed IntSkippedData = 0 -- Count of current skipped Tags IntDeletedData = 0 -- Count of current deleted Tags IntChangedData = 0 -- Count of current changed Tags IntWarningData = 0 -- Count of current warning Messages IntSkippedTotal = 0 -- Count of total skipped Tags IntDeletedTotal = 0 -- Count of total deleted Tags IntChangedTotal = 0 -- Count of total changed Tags IntWarningTotal = 0 -- Count of total warnings -- Global constants StrDeleteTag = "Delete" -- New Tag value to signal delete Old Tag StrSkipTag = "Skip" -- New Tag value to signal skip Old Tag StrModeOld = "Old" -- Parameter value to select only Old Tag StrModeNew = "New" -- Parameter value to select only New Tag StrModeAll = "All" -- Parameter value to select both Old & New Tag IntHelpCheck = 1 -- Parameter value to select Warning GUI Check Tags compatible message IntHelpNoted = 2 -- Parameter value to select Warning GUI Value in Source Note message IntHelpReport = 3 -- Parameter value to select Warning GUI Report to FHUG Forums message IntFilterValue = 1 -- Filter index for Tag selection Value Filter IntFilterDate = 2 -- Filter index for Tag selection Date Filter IntFilterPlace = 3 -- Filter index for Tag selection Place Filter IntFilterRecord = 4 -- Filter index for Records selection Filter -- V2.9 -- Predefined Tag Set Name constants for lookup tables DicType = { E = " (Events)"; A = " (Attributes)"; } -- Divide each Fact Set into Events & Attributes -- V3.7 StrStandard = "Standard" -- Standard Fact Set name, file & folder StrStandEvents = StrStandard..DicType.E -- Standrad Events Fcat Set Name -- V3.7 StrCustomEvents = "" -- Dummy Undefined Custom Events Set -- FH V7 -- V3.1 StrCustomAttrs = "" -- Dummy Undefined Custom Attributes Set -- FH V7 -- V3.1 StrDefinedTags = "" -- Dummy Gedcom 5.5 Defined Tags Set StrUDFTagsSet = "" -- Dummy Uncategorised Data Field (UDF) Tags Set TblRecordTag = {"INDI";"FAM";"NOTE";"SOUR";"REPO";"OBJE";"_PLAC";"SUBM";"SUBN";"_RNOT";"_SRCT";"HEAD";} TblRecordName = {"Individual";"Family";"Note";"Source";"Repository";"Multimedia";"Place";"Submitter";"Submission";"Resource Note";"Source Template";"Header";} -- Tag code & name table variables TblOldTagIndex = {} -- TblOldTagIndex[strRecTag][strTagSet][intItem] = strOldTag and TblOldTagIndex[strRecTag][strTagSet][strOldTag] = true for Source Name dropdown lists TblNewTagIndex = {} -- TblNewTagIndex[strRecTag][strTagSet][intItem] = strNewTag and TblNewTagIndex[strRecTag][strTagSet][strOldTag] = true for Target Name dropdown lists TblSetNameIndex = {} -- TblSetNameIndex[strRecTag][intItem] = strSetName for Source/Target Set dropdown lists TblDictionary = {} -- TblDictionary[strRecTag][strTag] = strName and TblDictionary[strRecTag][strTagSet][strName] = strTag both way translations for intRecTag,strRecTag in ipairs(TblRecordTag) do TblOldTagIndex [strRecTag] = {} TblNewTagIndex [strRecTag] = {} TblSetNameIndex[strRecTag] = {} TblDictionary [strRecTag] = {} for intItem, strSetName in ipairs({StrCustomAttrs;StrCustomEvents;StrDefinedTags;StrUDFTagsSet;}) do -- FH V7 -- V3.1 TblOldTagIndex[strRecTag][strSetName] = {} TblNewTagIndex[strRecTag][strSetName] = {} TblDictionary [strRecTag][strSetName] = {} -- V2.5 end -- Special RecordName v RecordTag both way translations TblDictionary[TblRecordName[intRecTag]] = strRecTag TblDictionary[strRecTag]["%"] = TblRecordName[intRecTag] end end -- function PresetGlobalConstants -- Reset Sticky Settings to Default Values -- function ResetDefaultSettings() iup_gui.CustomDialogue("Main","0x0") -- Custom "Main" dialogue minimum size & centralisation iup_gui.CustomDialogue("Font","0x0") -- Custom "Font" dialogue minimum size & centralisation iup_gui.CustomDialogue("Memo","0x0") -- Custom "Memo" dialogue minimum size & centralisation iup_gui.CustomDialogue("Logs","0x0") -- Custom "Logs" dialogue minimum size & centralisation iup_gui.DefaultDialogue("Bars","Memo","Logs") -- GUI window position X & Y co-ordinates, and minsize & rastersize for "Main","Font","Bars","Memo","Logs" dialogues StrLogNeed = "ON" -- Radio button "ON"/"OFF" states StrPattern = "ON" StrConfirm = "ON" StrWarning = "ON" end -- function ResetDefaultSettings -- Load Sticky Settings from File -- function LoadSettings() iup_gui.LoadSettings("Bars","Memo","Logs") -- Includes "Main","Font","Bars","Memo","Logs" dialogues and "FontSet" & "History" StrLogNeed = tostring(iup_gui.LoadGlobal("LogNeed",StrLogNeed)) StrPattern = tostring(iup_gui.LoadGlobal("RegExpr",StrPattern)) -- Legacy filter option StrPattern = tostring(iup_gui.LoadGlobal("Pattern",StrPattern)) StrConfirm = tostring(iup_gui.LoadGlobal("Confirm",StrConfirm)) StrWarning = tostring(iup_gui.LoadGlobal("Warning",StrWarning)) SaveSettings() -- Save sticky data settings end -- function LoadSettings -- Save Sticky Settings to File -- function SaveSettings() iup_gui.SaveGlobal("LogNeed",StrLogNeed) iup_gui.SaveGlobal("Pattern",StrPattern) iup_gui.SaveGlobal("Confirm",StrConfirm) iup_gui.SaveGlobal("Warning",StrWarning) iup_gui.SaveSettings("Bars","Memo","Logs") -- Includes "Main","Font","Bars","Memo","Logs" dialogues and "FontSet" & "History" end -- function SaveSettings -- Determine if a Table has Data -- function FlgTableHasData(tblTable) if next(tblTable) == nil then return false else return true end end -- function FlgTableHasData -- Insert a Tag in Old &/or New Tag Index of Tags defined by Tag Set function DoInsertTagIndex(strRecTag,strSet,strTag,strMode) -- strRecTag ~ Record Tag -- strSet ~ Tag Set name -- strTag ~ Tag name -- strMode ~ Insert mode:- -- = StrModeOld to only update Old Tag Index -- = StrModeNew to only update New Tag Index -- = StrModeAll to update both Old & New Index if strMode == StrModeOld or strMode == StrModeAll then local tblOldTagIndex = TblOldTagIndex[strRecTag][strSet] -- V2.5 if not tblOldTagIndex[strTag] then -- V2.5 tblOldTagIndex[strTag] = true table.insert(tblOldTagIndex,strTag) -- Add Tag to Old Tag list end end if strMode == StrModeNew or strMode == StrModeAll then local tblNewTagIndex = TblNewTagIndex[strRecTag][strSet] -- V2.5 if not tblNewTagIndex[strTag] then -- V2.5 tblNewTagIndex[strTag] = true table.insert(tblNewTagIndex,strTag) -- Add Tag to New Tag list end end end -- function DoInsertTagIndex -- Remove a Tag from Old Tag Index of Tags defined by Tag Set function DoRemoveTagIndex(strRecTag,strOldSet,strOldTag) -- strRecTag ~ Record Tag -- strOldSet ~ Source Set name -- strOldTag ~ Source Tag name local tblOldTagIndex = TblOldTagIndex[strRecTag][strOldSet] -- V2.5 if tblOldTagIndex[strOldTag] then -- V2.5 for intTag,strTag in ipairs(tblOldTagIndex) do if strTag == strOldTag then table.remove(tblOldTagIndex,intTag) tblOldTagIndex[strOldTag] = nil break end end end end -- function DoRemoveTagIndex -- Load Standard & Custom & UDF Facts & Tags -- function LoadFactsAndTags() -- Read any Group Index or Fact Set File -- V3.7 local function strReadFile(strFile) local strText if fhGetAppVersion() > 6 then strText = fhLoadTextFile(strFile,"UTF-16LE") -- FH V7 files are UTF-16 format else local strUtf16 = "^.%z" -- FH V6 or earlier caters for any format local bomUtf16 = "^"..string.char(0xFF,0xFE) -- "ÿþ" local bomUtf8 = "^"..string.char(0xEF,0xBB,0xBF) -- "" local fncConv = tostring -- Function to convert input to current encoding local strEncoding = string.encoding() local fHandle = general.OpenFile(strFile,"rb") strText = fHandle:read("*a") -- Read entire file if strText:match(bomUtf16) or strText:match(strUtf16) then strText = strText:gsub(bomUtf16,"") -- Strip UTF-16 BOM if strEncoding == "ANSI" then -- Define UTF-16 conversion to current encoding fncConv = encoder.StrUTF16_ANSI else fncConv = encoder.StrUTF16_UTF8 end elseif strText:match(bomUtf8) then strText = strText:gsub(bomUtf8,"") -- Strip UTF-8 BOM if strEncoding == "ANSI" then -- Define UTF-8 conversion to current encoding fncConv = encoder.StrUTF8_ANSI end else if strEncoding == "UTF-8" then -- Define ANSI conversion to current encoding fncConv = encoder.StrANSI_UTF8 end end strText = fncConv(strText) -- Convert the text end return strText end -- local function strReadFile -- Load a Facts File into Facts Tables local function doLoadFactsTable(dicFactSet) -- V3.0 local strFactsFile = dicFactSet.File -- V3.0 local strFactsName = dicFactSet.Name -- V3.0 local strRecTag, strOldTag, strFact, strType -- V2.6 -- V3.7 -- Parse a Fact Item from a Fact File local function strstrParseFactItem(strItem) -- Standard Facts have format such as TAG-IE or TAG-FA -- Custom Facts have format such as EVEN-TAG-IE or _ATTR-TAG-FA or FACT-TAG-FA -- FH V7 -- V3.1 -- Witness Roles have format such as TAG-IE-ROLE or TAG-FA-ROLE to be ignored local strTag,strEnd,strType,strRole = strItem:match("^(.+)%-([IF])([EA])([%-ROLE]-)$") -- V2.7 -- V3,7 if strRole == "-ROLE" then -- Ignore Witness Roles -- V2.7 return nil,nil elseif strEnd == "I" then -- "IA" Indiv Attr or "IE" Indiv Event return strTag, "INDI", DicType[strType] -- V3.7 elseif strEnd == "F" then -- "FA" Family Attr or "FA" Family Event return strTag, "FAM" , DicType[strType] -- V3.7 else error("\n\n Unrecognised fact item "..strItem.." \n in "..strFactsFile.." \n\n") end end -- local function strstrParseFactItem for _, strType in ipairs ({ "A"; "E"; }) do -- Create empty Fact Set tables -- V3.7 local strFactSet = strFactsName..DicType[strType] TblOldTagIndex["INDI"][strFactSet] = {} TblNewTagIndex["INDI"][strFactSet] = {} TblDictionary ["INDI"][strFactSet] = {} -- V2.5 TblOldTagIndex["FAM"] [strFactSet] = {} TblNewTagIndex["FAM"] [strFactSet] = {} TblDictionary ["FAM"] [strFactSet] = {} -- V2.5 end for strLine in strReadFile(strFactsFile):gmatch("[^\r\n]+") do -- V3.7 local strPref, strSuff = strLine:match("^([%[%a]-)%d-[=%-](.-)%]?$") -- V2.6 if strPref and strSuff then if strPref:match("^%[") and strRecTag and strOldTag and strFact then -- V2.8 DoInsertTagIndex(strRecTag,strFactsName..strType,strOldTag,StrModeNew) -- V3.7 TblDictionary[strRecTag][strOldTag] = strFact -- Save the Fact Tag details strRecTag = nil strOldTag = nil strFact = nil end if strPref == "Item" -- Found a Fact Tag item such as Item21=EVEN-TAG-IE or Item=TAG-FA or strPref == "[FCT" then -- Found start of Fact definition such as [FCT-TAG-IE] strOldTag,strRecTag,strType = strstrParseFactItem(strSuff) strFact = "" -- V2.5 elseif strPref == "Name" then -- Found Fact Name to save against Tag strFact = strSuff -- V2.5 elseif strPref == "Label" then -- Found Fact Label preferred to Fact Name if #strFact > IntMaxNameLen or #strSuff <= IntMaxNameLen then strFact = strSuff -- V2.5 end elseif strPref == "Abbr" and #strSuff > 0 then -- Found Fact Abbr to replace long Fact Name -- V3.0 if #strFact > IntMaxNameLen and #strSuff <= IntMaxNameLen then strFact = strSuff -- V2.6 end elseif strPref == "Hidden" and strSuff == "Y" then strFact = strFact.." " -- Fact Name is -- V2.8 end end end for _, strRecTag in ipairs({ "INDI"; "FAM"; }) do local tblSetNameIndex = TblSetNameIndex[strRecTag] for _, strType in ipairs({ "A"; "E"; }) do -- Save the Fact Set tables -- V3.7 local strFactSet = strFactsName..DicType[strType] if strFactsName:match(" %(project%)$") then table.insert(tblSetNameIndex,1,strFactSet) -- Ensure that a (project) fact set goes above Standard -- V3.5 elseif strFactSet ~= StrStandard..DicType[strType] then table.insert(tblSetNameIndex,3,strFactSet) -- Reverse order of Fact Sets, but with Standard at top -- V2.6 end for intOldTag,strOldTag in ipairs(TblNewTagIndex[strRecTag][strFactSet]) do local strName = TblDictionary[strRecTag][strOldTag] TblDictionary[strRecTag][strFactSet][strName] = strOldTag -- V2.5 end end end end -- local function doLoadFactsTable -- Load Gedcom Defined Tags to Dummy Tag Set local function doLoadDefinedTags(strRecTag,tblRecTag) for strOldTag,strOldName in pairs(tblRecTag) do DoInsertTagIndex(strRecTag,StrDefinedTags,strOldTag,StrModeNew) TblDictionary[strRecTag][strOldTag] = strOldName TblDictionary[strRecTag][StrDefinedTags][strOldName] = strOldTag -- V2.5 end end -- local function doLoadDefinedTags -- Load Undefined Tags to Dummy Tag Sets local function doLoadUndefinedTags() local strSuffix = "" local flgTagFound = false local strOldTag = "" local ptrOldTag = fhNewItemPtr() local ptrRecord = fhNewItemPtr() for intRecTag,strRecTag in ipairs(TblRecordTag) do ptrRecord:MoveToFirstRecord(strRecTag) while not ptrRecord:IsNull() do -- Search each Record ptrOldTag:MoveToFirstChildItem(ptrRecord) while not ptrOldTag:IsNull() do -- Search each Tag strOldTag = fhGetTag(ptrOldTag) if fhIsEvent(ptrOldTag) or fhIsAttribute(ptrOldTag) then -- Found an Event/Attribute Fact for intSet,strSet in ipairs(TblSetNameIndex[strRecTag]) do -- Search existing Fact Sets and Fact Tags flgTagFound = false if TblNewTagIndex[strRecTag][strSet][strOldTag] then -- V2.5 DoInsertTagIndex(strRecTag,strSet,strOldTag,StrModeOld) -- Add current Fact Tag to Old Tag index for Fact Set flgTagFound = true break -- Escape if Fact Tag is found end end if not flgTagFound then -- Add undefined Tag to dummy undefined Custom Events/Attributes fact set -- V3.1 local strOldDisp = fhGetDisplayText(ptrOldTag)..(" "):rep(9) -- Extract undefined fact name -- V3.1 -- V3.9 local _, intLast = strOldDisp:find("^(.-) ",#strOldTag-5) -- #strOldTag:match("^.-%-(.+)$")) local strOldName = strOldDisp:sub(1,intLast-1):gsub(" +$","") -- V3.9 local strCustom = StrCustomEvents if fhIsAttribute(ptrOldTag) then strCustom = StrCustomAttrs end DoInsertTagIndex(strRecTag,strCustom,strOldTag,StrModeAll) TblDictionary[strRecTag][strOldTag] = strOldName TblDictionary[strRecTag][strCustom][strOldName] = strOldTag -- V2.5 end elseif fhIsUDF(ptrOldTag) then -- Found UDF Tag strSuffix = " UDF Tag" if strRecTag == "INDI" or strRecTag == "FAM" then local strOldVal = fhGetValueAsText(ptrOldTag) local ptrLink,strTag = PtrStrTranslateLink(strOldVal) -- If value is a link such as @S99@ then not valid as a Fact if ptrLink == nil then strSuffix = " UDF Attribute" if strOldVal == "" and TblNewTagIndex[strRecTag][StrUDFTagsSet][strOldTag] ~= strOldTag..strSuffix then strSuffix = " UDF Event" -- If its value is null, and not already an Attribute, then it is an Event end end end DoInsertTagIndex(strRecTag,StrUDFTagsSet,strOldTag,StrModeAll) -- Add uncategorised Data Tag to dummy UDF Tag Set TblDictionary[strRecTag][strOldTag] = strOldTag..strSuffix TblDictionary[strRecTag][StrUDFTagsSet][strOldTag..strSuffix] = strOldTag -- V2.5 else DoInsertTagIndex(strRecTag,StrDefinedTags,strOldTag,StrModeOld) -- Add current defined Data Tag to Old Tag index if TblDictionary[strRecTag][strOldTag] == nil then TblDictionary[strRecTag][strOldTag] = strOldTag.." not Defined !" -- Fallback in case Tag is not in Set TblDictionary[strRecTag][StrDefinedTags][strOldTag.." not Defined !"] = strOldTag -- V2.5 end end ptrOldTag:MoveNext("ANY") end ptrRecord:MoveNext("SAME_TAG") -- Move to next Record end for intItem,strSetName in ipairs({StrCustomEvents;StrCustomAttrs;StrDefinedTags;StrUDFTagsSet;}) do if FlgTableHasData(TblOldTagIndex[strRecTag][strSetName]) then table.insert(TblSetNameIndex[strRecTag], strSetName) end end end end -- local function doLoadUndefinedTags() for _, strRecTag in ipairs({ "INDI"; "FAM"; }) do -- Ensure Standard is always top of list except for local (project) fact set -- V3.7 for _, strType in ipairs({ "A"; "E"; }) do local strFactSet = StrStandard..DicType[strType] table.insert(TblSetNameIndex[strRecTag], 1, strFactSet) end end -- Use the GroupIndex to process Fact Sets in reverse order to obscure Facts (like Export Gedcom File) -- V2.6 -- V3.0 local dicGroupIndex = {} local strDataFolder = fhGetContextInfo("CI_PROJECT_DATA_FOLDER").."\\Fact Types" local strGroupIndex = strDataFolder.."\\GroupIndex.fhdata" -- Only applies to FH V7 or later if general.FlgFileExists(strGroupIndex) then -- Read each line in Project only GroupIndex -- V3.2 -- V3.4 -- V3.7 for strFactSet, intFactSet in general.StrLoadFromFile(strGroupIndex):gmatch("([^\r\n]+)=(%d+)") do intFactSet = tonumber(intFactSet) -- Extract Fact Set name and ordinal position if strFactSet and intFactSet and not dicGroupIndex[intFactSet] then -- Compose Fact Set filename local strFactsFile = strDataFolder.."\\"..strFactSet..".fhf" if general.FlgFileExists(strFactsFile) then -- Fact Set name and file exist, so save in ordinal order dicGroupIndex[intFactSet] = { Name=strFactSet.." (project)"; File=strFactsFile; } end end end end local intGroupIndex = #dicGroupIndex local strDataFolder = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Fact Types" local strGroupIndex = strDataFolder.."\\Standard\\GroupIndex.fhdata" if general.FlgFileExists(strGroupIndex) then -- Read each line in ProgramData GroupIndex -- V3.0 -- V3.7 for strFactSet, intFactSet in strReadFile(strGroupIndex):gmatch("([^\r\n]+)=(%d+)") do intFactSet = tonumber(intFactSet) if strFactSet and intFactSet and not dicGroupIndex[intFactSet+intGroupIndex] then -- Compose Fact Set filename -- V3.0 local strFactsFile = strDataFolder.."\\Custom\\"..strFactSet..".fhf" if strFactSet == StrStandard then strFactsFile = strDataFolder.."\\Standard\\Standard.fhf" end if general.FlgFileExists(strFactsFile) then -- Fact Set name and file exist, so save in ordinal order -- V3.0 dicGroupIndex[intFactSet+intGroupIndex] = { Name=strFactSet; File=strFactsFile; } end end end end for intFactSet = #dicGroupIndex, 1, -1 do -- Reverse order of Fact Sets doLoadFactsTable(dicGroupIndex[intFactSet]) -- Import Fact Set file to Fact tables -- V3.0 end if not next(TblDictionary.INDI[TblSetNameIndex.INDI[1]]) then table.remove(TblSetNameIndex.INDI,1) end -- V3.7 if not next(TblDictionary.FAM [TblSetNameIndex.FAM [1]]) then table.remove(TblSetNameIndex.FAM ,1) end -- V3.7 -- Load common GEDCOM 5.5 1996 & Place Defined Tag Sets doLoadDefinedTags("INDI" ,{RIN="Automated Record Id";ALIA="Alias";ANCI="Ancestor Interest";ASSO="Associated Person";REFN="Custom Id";DESI="Descendant Interest";_FLGS="Flags";CHAN="Last Change Notes";AFN="LDS Ancestral File No.";BAPL="LDS Baptism";SLGC="LDS Child Sealing";CONL="LDS Confirmation";ENDL="LDS Endowment";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";NAME="Name";NOTE="Note Record";NOTE2="Note";RFN="Permanent Record No.";FAMC="Parents Family";FAMS="Spouse Family";RESN="Restriction Notice";SEX="Sex";SOUR="Source (citation)";SOUR2="Source Note";SUBM="Submitter";}) doLoadDefinedTags("FAM" ,{RIN="Automated Record Id";CHIL="Child";NCHI="Child Count";REFN="Custom Id";HUSB="Husband";CHAN="Last Change Notes";SLGS="LDS Spouse Sealing";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";_STAT="Marriage Status";NOTE="Note Record";NOTE2="Note";SOUR="Source (citation)";SOUR2="Source Note";SUBM="Submitter";WIFE="Wife";}) doLoadDefinedTags("NOTE" ,{RIN="Automated Record Id";TEXT="Text";REFN="Custom Id";CHAN="Last Change Notes";SOUR="Source (citation)";SOUR2="Source Note";}) doLoadDefinedTags("SOUR" ,{RIN="Automated Record Id";AUTH="Author";REFN="Custom Id";DATA="Data";CHAN="Last Change Notes";NOTE="Note Record";NOTE2="Note";PUBL="Publication Info";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";REPO="Repository";ABBR="Short Title";TEXT="Text From Source";TITL="Title";_TYPE="Type";}) doLoadDefinedTags("REPO" ,{RIN="Automated Record Id";ADDR="Address";REFN="Custom Id";CHAN="Last Change Notes";NAME="Name";NOTE="Note Record";NOTE2="Note";PHON="Phone Number";}) doLoadDefinedTags("OBJE" ,{RIN="Automated Record Id";REFN="Custom Id";_DATE="Date";_KEYS="Keywords";CHAN="Last Change Notes";NOTE="Note Record";}) doLoadDefinedTags("_PLAC",{CHAN="Last Change Notes";LATLONG="Lat./Longitude";OBJE="Multimedia Object";NOTE2="Note";TEXT="Place Name";STAN="Standardized";STAT="Status";}) doLoadDefinedTags("SUBM" ,{RIN="Automated Record Id";ADDR="Address";NAME="Name";LANG="Language";CHAN="Last Change Notes";OBJE="Multimedia Object";OBJE2="Multimedia Object (local)";PHON="Phone Number";RFN="Permanent Record No.";}) doLoadDefinedTags("SUBN" ,{RIN="Automated Record Id";ANCE="Ancestor Generations";DESC="Descendant Generations";FAMF="Family File Name";CHAN="Last Change Notes";ORDI="Ordinance Process";SUBM="Submitter";TEMP="Temple Code";}) doLoadDefinedTags("HEAD" ,{CHAR="Character Set";COPR="Copyright";DATE="Transmission Date";DEST="Receiving System";FILE="Filename";_ROOT="File Root";GEDC="GEDCOM";LANG="Language";NOTE2="File Description";PLAC="Place Format";_ROOT="File Root";_USED="Last-used Record Id";SOUR2="System Id";SUBM="Submitter";SUBN="Submission";_UID="User Id";}) if fhGetAppVersion() < 7 then -- Load variants for GEDCOM 5.5 1996 Defined Tag Sets -- FH V7 -- V3.1 doLoadDefinedTags("REPO" ,{_EMAIL="Email";_WEB="Web Site";}) doLoadDefinedTags("OBJE" ,{BLOB="Bulk Object";_FILE="File";FORM="Format";_NOTE="Note";NOTE2="Link/Note";TITL="Title";}) -- OBJE="Multimedia Link" associated only with BLOB continuation doLoadDefinedTags("SUBM" ,{_EMAIL="Email";_WEB="Web Site";}) else -- Load variants for GEDCOM 5.5.1 2019 & Research Note & Source Template Defined Tag Sets -- FH V7 -- V3.1 doLoadDefinedTags("INDI" ,{_RNOT="Research Note";_FSID="FamilySearch Id";_UID="Unique Id";}) -- V3.6 doLoadDefinedTags("FAM" ,{_RNOT="Research Note";}) doLoadDefinedTags("SOUR" ,{_RNOT="Research Note";_SRCT="Source Template";_FIELD="Metafield"}) -- V3.3 doLoadDefinedTags("REPO" ,{_RNOT="Research Note";EMAIL="Email";WWW="Web Site";}) doLoadDefinedTags("_PLAC",{_RNOT="Research Note";}) doLoadDefinedTags("OBJE" ,{_RNOT="Research Note";_NOTA="Annotation";FILE="File";NOTE2="Note";}) doLoadDefinedTags("SUBM" ,{EMAIL="Email";WWW="Web Site";}) doLoadDefinedTags("HEAD" ,{_PCIT="Prepared Citation";}) -- V3.3 doLoadDefinedTags("_RNOT",{REFN="Custom Id";CHAN="Last Change Notes";TEXT="Text";}) doLoadDefinedTags("_SRCT",{BIBL="Bibliography";CATG="Category";COLL="Collection";DESC="Description";FDEF="Field Definition";FOOT="Footnote";CHAN="Last Change Notes";NAME="Name";NOTE2="Note";TITL="Record Title";REFN="Reference";SHRT="Short Footnote";SUBC="Subcategory";}) end -- Load any undefined Custom Fact or Uncategorised Data Field (UDF) into dummy Tag Sets doLoadUndefinedTags() end -- function LoadFactsAndTags -- Write Line to Log -- -- V3.7 function WriteLog(strLine) if StrLogNeed == "ON" then -- Logging enabled local strBOM = "" if string.encoding() == "UTF-8" then strBOM = string.char(0xEF,0xBB,0xBF) -- UTF-8 BOM end if LblLogFile.Title ~= StrLogFilePath then -- Log has just been created LblLogFile.Title = StrLogFilePath -- Report log file path in Main GUI ArrLogFileText = {} table.insert(ArrLogFileText,strBOM.."Folder\t"..StrProjectPath) -- Insert folder & name details into log table.insert(ArrLogFileText,"Logfile\t"..StrLogFileName) table.insert(ArrLogFileText," ") end table.insert(ArrLogFileText,strLine) -- Write data line to log end end -- function WriteLog -- Update Count Status -- function UpdateCountStatus() local function strCountFormat(intCount,strFormat) if strFormat == "Warnings." and intCount == 1 then strFormat = "Warning. " end strFormat = string.format("%4d ",intCount)..strFormat return strFormat end -- local function strCountFormat local strCounts = "" -- Counts log report local strTotals = "" -- Totals log report local strFormat = "" -- Formatted data -- Report Data Counts strFormat = strCountFormat(IntSkippedData,"Skipped.") LblCoSkipped.Title = strFormat strCounts = strCounts..strFormat.."\t" strFormat = strCountFormat(IntDeletedData,"Deleted.") LblCoDeleted.Title = strFormat strCounts = strCounts..strFormat.."\t" strFormat = strCountFormat(IntChangedData,"Changed.") LblCoChanged.Title = strFormat strCounts = strCounts..strFormat.."\t" strFormat = strCountFormat(IntWarningData,"Warnings.") LblCoWarning.Title = strFormat strCounts = strCounts..strFormat -- Report Total Counts strFormat = strCountFormat(IntSkippedTotal,"Skipped.") LblToSkipped.Title = strFormat strTotals = strTotals..strFormat.."\t" strFormat = strCountFormat(IntDeletedTotal,"Deleted.") LblToDeleted.Title = strFormat strTotals = strTotals..strFormat.."\t" strFormat = strCountFormat(IntChangedTotal,"Changed.") LblToChanged.Title = strFormat strTotals = strTotals..strFormat.."\t" strFormat = strCountFormat(IntWarningTotal,"Warnings.") LblToWarning.Title = strFormat strTotals = strTotals..strFormat WriteLog("Counts\t"..strCounts) --? WriteLog("Totals\t"..strTotals) WriteLog(" ") return strCounts:gsub("\t"," ").."\r\n" end -- function UpdateCountStatus -- Get Display of Record Name -- function StrDisplayName(ptrName) return fhGetDisplayText(ptrName).." ["..fhGetRecordId(ptrName).."]" end -- function StrDisplayName -- Get Display of Record Data -- function StrDisplayData(ptrData) return general.BuildDataRef(ptrData)..": "..fhGetDisplayText(ptrData) -- V2.5 end -- function StrDisplayData -- Global tables for Query Result Set Columns TblPtrName = {} TblOldItem = {} TblOldData = {} TblPerform = {} TblNewItem = {} TblPtrData = {} -- Write Tag Change to Log and Increment Counts -- function WriteDataChange(ptrName,ptrOld,ptrNew,strNew) -- ptrName ~ Record pointer -- ptrOld ~ Source Tag pointer -- ptrNew ~ Target Tag pointer or StrDeleteTag -- strNew ~ "" or StrSkipTag or StrDeleteTag WriteLog("Name\t'"..TblDictionary[fhGetTag(ptrName)]["%"]..": "..StrDisplayName(ptrName).."'") local strLine = "Data\t'"..StrDisplayData(ptrOld) table.insert(TblPtrName,ptrName:Clone()) table.insert(TblOldItem,StrOldData) table.insert(TblOldData,StrDisplayData(ptrOld)) table.insert(TblPerform,"unknown") table.insert(TblNewItem,"") table.insert(TblPtrData,"") if strNew == StrDeleteTag then IntDeletedData = IntDeletedData + 1 IntDeletedTotal = IntDeletedTotal + 1 strLine = strLine.."' deleted." TblPerform[#TblPtrName] = "deleted" elseif strNew == StrSkipTag then IntSkippedData = IntSkippedData + 1 IntSkippedTotal = IntSkippedTotal + 1 strLine = strLine.."' skipped." TblPerform[#TblPtrName] = "skipped" TblPtrData[#TblPtrName] = ptrOld:Clone() else IntChangedData = IntChangedData + 1 IntChangedTotal = IntChangedTotal + 1 strLine = strLine.."' became '"..StrDisplayData(ptrNew).."'" TblPerform[#TblPtrName] = "changed" TblNewItem[#TblPtrName] = StrNewData TblPtrData[#TblPtrName] = ptrNew:Clone() end WriteLog(strLine) end -- function WriteDataChange -- Translate NOTE/OBJE/SOUR link @@N99@@ or @O99@ or @@S99@@ into a Pointer and Tag -- function PtrStrTranslateLink(strLink) local ptrLink = fhNewItemPtr() -- return Null pointer if no link found local strTag = nil if strLink:sub(1,1) == "@" and strLink:sub(-1) == "@" then strLink = strLink:gsub("@","") -- Starts & ends with @ so looks like a link that needs every @ removed local tblTag = { N="NOTE"; O="OBJE"; S="SOUR"; } strTag = tblTag[strLink:sub(1,1)] -- Check and convert initial letter N or O or S to Tag if strTag then -- V2.5 strLink = strLink:sub(2) -- Remove initial letter to leave number ID ptrLink = fhNewItemPtr() ptrLink:MoveToRecordById(strTag,tonumber(strLink)) -- Obtain link to record using number ID end end return ptrLink,strTag end -- function PtrStrTranslateLink -- Edit the Data Details for Source Tag and Target Tag -- function DoEditDataDetails(ptrRecord,ptrOld,strNew) -- ptrRecord ~ Record pointer -- ptrOld ~ Source Tag pointer -- strNew ~ Target Tag or StrDeleteTag local ptrNew = fhNewItemPtr() local isCont = true -- Continue changes after Warning -- V3.8 local tblLog = {} -- Log of warnings for SOURce Notes local intLog = 0 -- Log count for warnings local tblTag = {} -- Tag data ref hierarchy local intTag = 1 -- Tag level for hierarchy tblTag[intTag] = fhGetTag(ptrRecord) -- "INDI","FAM","NOTE","SOUR",etc -- Compose Current Data Reference -- local function strDataReference(strTag) local strDataRef = "%" for intItem, strName in ipairs(tblTag) do strDataRef = strDataRef..strName.."." end strDataRef = strDataRef..strTag.."%" return strDataRef end -- local function strDataReference -- Log and Report Warning Message -- local function doReportWarning(strTag,strVal,strWarn,intHelp) -- Compose new Data Reference & Value & Warning message strWarn = strDataReference(strTag).." "..strVal.." ~ "..strWarn intLog = intLog + 1 tblLog[intLog] = strWarn IntWarningData = IntWarningData + 1 IntWarningTotal = IntWarningTotal + 1 if StrWarning == "ON" then isCont = GUI_WarnDialogue(StrDisplayName(ptrRecord),StrDisplayData(ptrOld),strWarn,intHelp) -- V3.8 end end -- local function doReportWarning -- Copy All Children Items -- local function doCopyChildrenItems(ptrOld,ptrNew) -- Copy All Child Branch Items -- local function doCopyChildBranch(ptrOrigin,ptrTarget) local strTag = fhGetTag(ptrOrigin) local strVal = fhGetValueAsText(ptrOrigin) --? local lnkVal = fhGetValueAsLink(ptrOrigin) -- Diagnostic --? local iValue = fhGetValueAsInteger(ptrOrigin) -- Diagnostic --? local dtDate = fhGetValueAsDate(ptrOrigin) -- Diagnostic --? local strTxt = fhGetItemText(ptrOrigin,strTag) -- Diagnostic --? local strType = fhGetValueType(ptrOrigin) -- Diagnostic --? local strClas = fhGetDataClass(ptrOrigin) -- Diagnostic local ptrNew = fhNewItemPtr() local function doConvertUDF_Link() local ptrLink,strLink = PtrStrTranslateLink(strVal) -- Convert UDF SOUR/OBJE/NOTE Link to Pointer & Tag if ptrLink:IsNotNull() then ptrNew = fhCreateItem(strLink,ptrTarget) if ptrNew:IsNull() then doReportWarning(strLink,strVal,"Invalid Tag",IntHelpCheck) else if not fhSetValueAsLink(ptrNew,ptrLink) then -- Set Link Pointer as new Tag value doReportWarning(strLink,strVal,"Link Pointer value not accepted",IntHelpCheck) end doCopyChildrenItems(ptrOrigin,ptrNew) end return nil end strTag = strTag.."2" -- Convert UDF SOUR/OBJE/NOTE to text Tag return strTag end -- local function doConvertUDF_Link local function doConvertUDF_Date() ptrNew = fhCreateItem(strTag,ptrTarget) if ptrNew:IsNull() then doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck) else local dtDate = fhNewDate(1999) -- Convert Date string to Date Object if not dtDate:SetValueAsText(strVal,false) then doReportWarning(strTag,strVal,"Unrecognised 'Date' value, treated as 'Date Phrase'",IntHelpNoted) if not dtDate:SetValueAsText(strVal,true) then doReportWarning(strTag,strVal,"Unrecognised 'Date Phrase' value",IntHelpNoted) else if not fhSetValueAsDate(ptrNew,dtDate) then -- Set Date Object as new Tag value doReportWarning(strTag,strVal,"Date Object value not accepted",IntHelpCheck) end end else if not fhSetValueAsDate(ptrNew,dtDate) then -- Set Date Object as new Tag value doReportWarning(strTag,strVal,"Date Object value not accepted",IntHelpCheck) end end doCopyChildrenItems(ptrOrigin,ptrNew) end return nil end -- local function doConvertUDF_Date local function doConvertUDF_Even() ptrNew = fhCreateItem(strTag,ptrTarget) if ptrNew:IsNull() then doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck) else local tblEvent = {} -- EVENt can be EVEN, ADOP, BIRT, BAPM, CAST, etc, etc local strEvent = nil tblEvent[1] = { EVEN="Event"; CAST="Social Rank"; CHRA="Adult Christening"; } -- Exceptions not translated below tblEvent[2] = TblDictionary["INDI"] -- All the Individual Fact tag translations tblEvent[3] = TblDictionary["FAM"] -- All the Family Fact tag translations for i=1,3 do strEvent = tblEvent[i][strVal] if strEvent then break end -- V2.5 end if strEvent == nil then doReportWarning(strTag,strVal,"Unrecognised 'Event Type' value",IntHelpReport) else if not fhSetValueAsText(ptrNew,strEvent) then doReportWarning(strTag,strEvent,"Event Type text not accepted",IntHelpCheck) end end doCopyChildrenItems(ptrOrigin,ptrNew) end return nil end -- local function doConvertUDF_Even local function doConvertUDF_Quay() ptrNew = fhCreateItem(strTag,ptrTarget) if ptrNew:IsNull() then doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck) else -- QUAY Certainty can be "0" ; "1" ; "2" ; "3" ; local tblCertainty = {"Unreliable";"Questionable";"Secondary evidence";"Primary evidence";} local strCertainty = tblCertainty[tonumber(strVal)+1] if strCertainty == nil then doReportWarning(strTag,strVal,"Unrecognised 'Certainty' value",IntHelpReport) else if not fhSetValueAsText(ptrNew,strCertainty) then doReportWarning(strTag,strCertainty,"Certainty text not accepted",IntHelpCheck) end end doCopyChildrenItems(ptrOrigin,ptrNew) end return nil end -- local function doConvertUDF_Quay local function doConvertUDF_Conc_Cont() -- CONC & CONT are always invalid if intTag == 2 and ( fhIsEvent(ptrTarget) or fhGetTag(ptrTarget) == "RESI" ) then strTag = "SOUR2" -- If at target level 2 of Event or RESI then return strTag -- Create SOUR2 Note as if Attribute value end local strValue = "\n" -- CONTinuation on a new line if strTag == "CONC" then strValue = "" -- CONCatenation on same line end local strValue = fhGetValueAsText(ptrTarget)..strValue..strVal if not fhSetValueAsText(ptrTarget,strValue) then -- Append to existing Target value doReportWarning(strTag,strValue,"Value text not accepted",IntHelpCheck) end if fhGetValueAsText(ptrTarget) ~= strValue then -- "\n" does not work for Attribute value ( nor ADDR tag !!!! ) strValue = fhGetValueAsText(ptrTarget).."~"..strVal if not fhSetValueAsText(ptrTarget,strValue) then doReportWarning(strTag,strValue,"Value text not accepted",IntHelpCheck) end end return nil end -- local function doConvertUDF_Conc_Cont if fhIsUDF(ptrOrigin) then if strTag == "SOUR" or strTag == "OBJE" or strTag == "NOTE" then -- Convert UDF SOUR/OBJE/NOTE link or text if doConvertUDF_Link() == nil then return end elseif strTag == "DATE" or strTag == "_DATE" then -- Convert UDF DATE/_DATE string to DATE object if doConvertUDF_Date() == nil then return end elseif strTag == "EVEN" then -- Convert UDF EVEN string if doConvertUDF_Even() == nil then return end elseif strTag == "QUAY" then -- Convert UDF QUAY string if doConvertUDF_Quay() == nil then return end elseif strTag == "CONC" or strTag == "CONT" then -- Convert UDF CONC or CONT tag if doConvertUDF_Conc_Cont() == nil then return end end end if strTag == "_FMT" then return end -- Skip rich text format code -- V3.3 -- No existing Child Tag so create new Child Tag if strTag == "TYPE" then -- Transfer any TYPE Descriptor to a SOURce Note ptrNew = fhCreateItem("SOUR2",ptrTarget) -- Because it cannot transfer to Custom Facts if ptrNew:IsNull() then doReportWarning("SOUR2",strVal,"Invalid Tag",IntHelpCheck) else if not fhSetValueAsText(ptrNew,"TYPE Descriptor: "..strVal) then doReportWarning("SOUR2","TYPE Descriptor: "..strVal,"Value text not accepted",IntHelpCheck) end end else if strTag == "_FIELD" then -- Substitute metafield shortcut -- V3.3 strTag = fhGetMetafieldShortcut(ptrOrigin) end ptrNew = fhCreateItem(strTag,ptrTarget,true) -- V5.0.2 lets existing empty items to be used, e.g. FILE or FORM auto-created by local OBJEct parent, if ptrNew:IsNull() then -- but FILE may be _FILE within OBJE within UDF, so try Tag without leading underscore. ptrNew = fhCreateItem(strTag:gsub("^_",""),ptrTarget,true) end if ptrNew:IsNull() then doReportWarning(strTag,strVal,"Invalid Tag",IntHelpCheck) else -- DATA & OBJE2 tags within UDF have null string text value that causes fhSetValue_Copy to fail if not ( fhGetValueType(ptrOrigin) == "text" and strVal == "" ) then local strValue = fhGetValueAsText(ptrOrigin) if not fhSetValue_Copy(ptrNew,ptrOrigin) then -- Otherwise just copy the Tag and Value doReportWarning(strTag,fhGetValueAsText(ptrOrigin),"Value not accepted",IntHelpCheck) end end end end doCopyChildrenItems(ptrOrigin,ptrNew) end -- local function doCopyChildBranch intTag = intTag + 1 -- Compile table of New Tag Data Reference hierarchy tblTag[intTag] = fhGetTag(ptrNew) local ptrOrigin = fhNewItemPtr() ptrOrigin = ptrOld:Clone() ptrOrigin:MoveToFirstChildItem(ptrOrigin) while ptrOrigin:IsNotNull() do doCopyChildBranch(ptrOrigin,ptrNew) -- Copy any Child Branch data ptrOrigin:MoveNext() end tblTag[intTag] = nil intTag = intTag - 1 end -- local function doCopyChildrenItems -- Delete Item and Report if Fails -- local function doDeleteItem(ptrTag,strVal,strFunc) local strTag = fhGetTag(ptrTag) if not fhDeleteItem(ptrTag) then doReportWarning(strTag,strVal,"fhDeleteItem failed in "..strFunc,IntHelpReport) end end -- local function doDeleteItem -- Copy the Tag Value -- local function flgCopyTagValue() local strTag = fhGetTag(ptrOld) local strVal = fhGetValueAsText(ptrOld) local ptrLnk = fhGetValueAsLink(ptrOld) local intVal = fhGetValueAsInteger(ptrOld) local dtDate = fhGetValueAsDate(ptrOld) local strOldText = fhGetItemText(ptrOld,strTag) local strOldType = fhGetValueType(ptrOld) -- Type strings "", "text", "richtext", "link", "integer", "date" local strOldClas = fhGetDataClass(ptrOld) local strNewType = fhGetValueType(ptrNew) local strNewClas = fhGetDataClass(ptrNew) -- If converting Event to Attribute is there an _UNCAT value that can be used as Attribute value -- V3.3 if fhIsEvent(ptrOld) and fhIsAttribute(ptrNew) then local strUncat = fhGetItemText(ptrOld,"~._UNCAT") if #strUncat > 0 then strVal = strUncat strOldText = strUncat strOldType = "text" local ptrUncat = fhGetItemPtr(ptrOld,"~._UNCAT") fhDeleteItem(ptrUncat) end end -- Omit all Event/RESIdence Facts where strVal == strOldType == "" if strOldType ~= "" then if strOldType == "text" then -- Attribute Fact or UDF Tag if strVal == "" then return true end local strLnk -- Try to convert old Tag text Value into a Link Pointer ptrLnk,strLnk = PtrStrTranslateLink(strVal) if ptrLnk:IsNotNull() then strOldType = "link" strTag = strLnk else intVal = tonumber(strVal) -- Try to convert old Tag text Value into an Integer if intVal ~= nil and strNewType == "integer" then strOldType = "integer" elseif dtDate:SetValueAsText(strVal) -- Try to convert old Tag text Value into a Date Object and strNewType == "date" then strOldType = "date" end end end if strOldType == "link" and ( fhIsAttribute(ptrNew) or fhIsEvent(ptrNew) ) then local ptrNew = fhCreateItem(strTag,ptrNew) -- Create a Fact child SOUR/OBJE/NOTE Record link if ptrNew:IsNull() then doReportWarning(strTag,strVal,"Invalid Tag ~ Tag Skipped",IntHelpCheck) return false else if not fhSetValueAsLink(ptrNew,ptrLnk) then -- Set the Link Pointer as its Value doReportWarning(strTag,strVal,"Invalid Link ~ Tag Skipped",IntHelpCheck) doDeleteItem(ptrNew,strVal,"flgCopyTagValue") return false end end elseif strNewType == "text" then -- Set new Tag value to old Tag value converted to Text if strOldType == "richtext" then -- V3.3 strOldText = fhGetValueAsRichText(ptrOld):GetPlainText() end if not fhSetValueAsText(ptrNew,strOldText) then doReportWarning(strTag,strOldText,"Invalid Text for "..strNew.." ~ Tag Skipped",IntHelpCheck) return false end elseif strNewType == "richtext" then -- Set new Tag value to old Tag value converted to Rich Text -- V3.3 local strRichText = fhNewRichText() if strOldType == "richtext" then strRichText = fhGetValueAsRichText(ptrOld) else strRichText:SetText(strOldText) end if not fhSetValueAsRichText(ptrNew,strRichText) then doReportWarning(strTag,strOldText,"Invalid Text for "..strNew.." ~ Tag Skipped",IntHelpCheck) return false end elseif strNewType == "" then -- New Tag is an Event, or RESIdence, so create SOURce Note for Value local ptrVal = fhNewItemPtr() ptrVal = fhCreateItem("SOUR2",ptrNew) if ptrVal:IsNull() then doReportWarning(strTag,strOldText,"Invalid Tag ~ Tag Skipped",IntHelpCheck) return false else -- Set new SOURce Note value to old Tag name and value converted to Text if not fhSetValueAsText(ptrVal,StrOldData.."="..strOldText) then doReportWarning(strTag,strOldText,"Invalid Text ~ Tag Skipped",IntHelpCheck) doDeleteItem(ptrVal,strOldText,"flgCopyTagValue") return false end end elseif strNewType == strOldType then if strNewType == "link" then -- Set the Link Pointer as current Tag value if not fhSetValueAsLink(ptrNew,ptrLnk) then doReportWarning(strTag,strOldText,"Invalid Link for "..strNew.." ~ Tag Skipped",IntHelpCheck) return false end elseif strNewType == "integer" then -- Set the Integer Value as current Tag value if not fhSetValueAsInteger(ptrNew,intVal) then doReportWarning(strTag,strOldText,"Invalid Integer for "..strNew.." ~ Tag Skipped",IntHelpCheck) return false end elseif strNewType == "date" then -- Set the Date Object as current Tag value if not fhSetValueAsDate(ptrNew,dtDate) then doReportWarning(strTag,strOldText,"Invalid Date for "..strNew.." ~ Tag Skipped",IntHelpCheck) return false end else doReportWarning(strTag,strOldText,"Invalid Tag Type: "..strNewType.." ~ Tag Skipped",IntHelpReport) return false end else if not fhSetValue_Copy(ptrNew,ptrOld) then -- Expected to fail doReportWarning(strTag,strVal,"Invalid value for "..strNew.." ~ Tag Skipped",IntHelpCheck) return false end end end return true end -- local function flgCopyTagValue if strNew == StrDeleteTag then -- Delete the old Tag from Record WriteDataChange(ptrRecord,ptrOld,strNew,StrDeleteTag) doDeleteItem(ptrOld,StrDeleteTag,"DoEditDataDetails") return isCont -- Continue after Warning? -- V3.8 end ptrNew = fhCreateItem(strNew,ptrRecord) -- Ensure the New Tag has been created if ptrNew:IsNull() then doReportWarning(strNew,TblDictionary[fhGetTag(ptrRecord)][strNew],"Cannot be created ~ Tag Skipped",IntHelpCheck) else if not fhMoveItemBefore(ptrNew,ptrOld) then -- Ensure the Tag order is maintained doReportWarning(strNew,TblDictionary[fhGetTag(ptrRecord)][strNew],"Cannot be moved ~ Tag Skipped",IntHelpCheck) doDeleteItem(ptrNew,"MoveItemBefore","DoEditDataDetails") else if not flgCopyTagValue() then -- Transfer old Tag value to new Tag value, or new SOURce Note doDeleteItem(ptrNew,"CopyTagValue","DoEditDataDetails") else doCopyChildrenItems(ptrOld,ptrNew) -- Copy the Child Tag structure WriteDataChange(ptrRecord,ptrOld,ptrNew,"") -- Log the Change doDeleteItem(ptrOld,"CopyTagValue","DoEditDataDetails") -- Delete the Old Tag from Record for intItem, strLog in ipairs(tblLog) do WriteLog("Warning\t"..strLog) -- Log each Warning Message local ptrVal = fhNewItemPtr() ptrVal = fhCreateItem("SOUR2",ptrNew) -- Copy each Warning into Tag SOURce Note if ptrVal:IsNull() then ptrVal = fhCreateItem("SOUR2",ptrRecord) -- or into whole Record SOURce Note if ptrVal:IsNull() then ptrVal = fhCreateItem("NOTE2",ptrRecord) -- or into whole Record local NOTE if ptrVal:IsNull() then doReportWarning("SOUR2/NOTE2","strLog","Warning cannot be created",IntHelpReport) return end end end if not fhSetValueAsText(ptrVal,strLog) then -- Save the Warning in Source/Local Note doReportWarning("SOUR2/NOTE2",strLog,"Warning cannot be saved",IntHelpReport) end end return isCont -- Continue after Warning? -- V3.8 end end end WriteDataChange(ptrRecord,ptrOld,ptrNew,StrSkipTag) -- Log the Skip for intItem, strLog in ipairs(tblLog) do WriteLog("Warning\t"..strLog) -- Log each Warning Message end return isCont -- Continue after Warning? -- V3.8 end -- function DoEditDataDetails -- Check if Filters Match the Selected Tag -- function FlgFiltersMatch(ptrTag,tblFilter) local flgFilter -- Determines whether Plain=true or Pattern=false filter matching if StrPattern == "OFF" then flgFilter = true else flgFilter = false end -- Check if Filter Value matches Record Value and fail if no match if string.find(fhGetItemText(ptrTag,fhGetTag(ptrTag)),tblFilter[IntFilterValue],1,flgFilter) == nil then return false end local strMatchDate = tblFilter[IntFilterDate] local flgMatchDate -- Determines if Filter Date matches a DATE field local strMatchPlace = tblFilter[IntFilterPlace] local flgMatchPlace -- Determines if Filter Place matches a PLAC field -- An empty Filter always matches, else if no field exists then it never matches if strMatchDate == "" then flgMatchDate = true else flgMatchDate = false end if strMatchPlace == "" then flgMatchPlace = true else flgMatchPlace = false end if not ( flgMatchDate and flgMatchPlace ) then -- Search the Child Tags for other Filters local ptrChild = fhNewItemPtr() ptrChild:MoveToFirstChildItem(ptrTag) while ptrChild:IsNotNull() do local strTag = fhGetTag(ptrChild) -- V2.6 if strTag == "DATE" and not flgMatchDate then -- Check if Filter Date matches DATE value and fail if no match if string.find(fhGetItemText(ptrChild,"DATE:COMPACT"),strMatchDate,1,flgFilter) == nil -- V2.5 added :COMPACT and string.find(fhGetItemText(ptrChild,"DATE:LONG"),strMatchDate,1,flgFilter) == nil then return false end flgMatchDate = true end if strTag:match("^_?PLAC$") and not flgMatchPlace then -- Check if Filter Place matches PLACe/_PLACe value and fail if no match -- V2.6 cater for _PLAC if string.find(fhGetValueAsText(ptrChild),strMatchPlace,1,flgFilter) == nil and string.find(fhGetValueAsText(fhGetItemPtr(ptrTag,"~._PLAC")),strMatchPlace,1,flgFilter) == nil then return false end -- V2.6 _PLAC flgMatchPlace = true end ptrChild:MoveNext() end if not ( flgMatchDate and flgMatchPlace ) then -- DATE or PLAC not matched at Child level so check Grandchild level ptrChild:MoveToFirstChildItem(ptrTag) while ptrChild:IsNotNull() do local ptrGrandChild = fhNewItemPtr() ptrGrandChild:MoveToFirstChildItem(ptrChild) while ptrGrandChild:IsNotNull() do local strTag = fhGetTag(ptrGrandChild) -- V2.6 if strTag == "DATE" and not flgMatchDate then -- Check if Filter Date matches DATE value and fail if no match if string.find(fhGetItemText(ptrGrandChild,"DATE:COMPACT"),strMatchDate,1,flgFilter) == nil -- V2.5 added :COMPACT and string.find(fhGetItemText(ptrGrandChild,"DATE:LONG"),strMatchDate,1,flgFilter) == nil then return false end flgMatchDate = true end if strTag == "PLAC" and not flgMatchPlace then -- Check if Filter Place matches PLACe value and fail if no match if string.find(fhGetValueAsText(ptrGrandChild),strMatchPlace,1,flgFilter) == nil then return false end flgMatchPlace = true end ptrGrandChild:MoveNext() end ptrChild:MoveNext() end end end return flgMatchDate and flgMatchPlace end -- function FlgFiltersMatch -- Change Selected Tag -- function DoChangeData(strRecTag,strOldSet,strNewSet,strOldTag,strNewTag,tblFilter) -- strRecTag ~ "INDI","FAM","SOUR",etc record type of Tag to change/delete -- strOldSet ~ Source Tag Set of Source Tag to change from or delete -- strNewSet ~ Target Tag Set of Target Tag to change into -- strOldTag ~ Source Tag Name to change from, or to delete -- strNewTag ~ Target Tag Name to change into, or StrDeleteTag to delete Source -- tblFilter ~ List of Filter Value, Date, Place text strings, and Records -- Iterate selected or all Records -- -- V2.9 local function ptrRecords(strRecTag,arrRecords) local ptrRec = fhNewItemPtr() local ptrNxt = fhNewItemPtr() local intRec if #arrRecords > 0 then intRec = 1 ptrRec = arrRecords[intRec] -- 1st selected record else ptrRec:MoveToFirstRecord(strRecTag) -- 1st of all records end return function () ptrNxt = ptrRec:Clone() -- Next record to return if intRec then intRec = intRec + 1 -- Next selected record ptrRec = arrRecords[intRec] or fhNewItemPtr() else ptrRec:MoveNext() -- Next of all records end if ptrNxt:IsNotNull() then return ptrNxt end end end -- local function ptrRecords -- Translate Tag to its Name -- local function strTranslateTag(strTag) local strData if strTag == StrDeleteTag then return strTag end strData = TblDictionary[strRecTag][strTag] strData = strData:replace("_ATTR-","") strData = strData:replace("FACT-","") -- FH V7 -- V3.1 strData = strData:replace("EVEN-","") return strData end -- local function strTranslateTag StrOldData = strTranslateTag(strOldTag) StrNewData = strTranslateTag(strNewTag) local flgContinue= true -- Set false by Warning dialogue Abort Changes button -- V3.8 local flgConvert = true -- Enable the Tag change/delete conversion operations local flgConfirm = ( StrConfirm == "ON" ) -- Enable/disable the GUI Data confirmation dialogue local ptrOldTag = fhNewItemPtr() -- Pointer to source Tag local ptrGotTag = fhNewItemPtr() -- Pointer to current Tag local ptrNewTag = fhNewItemPtr() -- Pointer to target Tag local ptrRecord = fhNewItemPtr() -- Pointer to current Record local intTagTotal = 0 -- Total matching source Tags for ptrRecord in ptrRecords(strRecTag,tblFilter[IntFilterRecord]) do -- V2.9 ptrOldTag:MoveTo(ptrRecord,"~."..strOldTag) while ptrOldTag:IsNotNull() do intTagTotal = intTagTotal + 1 -- Count matching source Tags ptrOldTag:MoveNext("SAME_TAG") end end if intTagTotal > 100 and not flgConfirm then progbar.Start("Editing Tags",intTagTotal) -- Optionally start Progress Bar end local intTagCount = intTagTotal -- Count pending source Tags IntSkippedData = 0 -- Count skipped source Tags IntDeletedData = 0 -- Count deleted source Tags IntChangedData = 0 -- Count changed source Tags IntWarningData = 0 -- Count warning Messages if tblFilter[IntFilterValue]..tblFilter[IntFilterDate]..tblFilter[IntFilterPlace] ~= "" then WriteLog("Filters\tValue="..tblFilter[IntFilterValue].." Date="..tblFilter[IntFilterDate].." Place="..tblFilter[IntFilterPlace]) end for ptrRecord in ptrRecords(strRecTag,tblFilter[IntFilterRecord]) do -- V2.9 ptrOldTag:MoveTo(ptrRecord,"~."..strOldTag) while ptrOldTag:IsNotNull() do ptrGotTag = ptrOldTag:Clone() -- Clone got Tag from old Tag ptrOldTag:MoveNext("SAME_TAG") if FlgFiltersMatch(ptrGotTag,tblFilter) then if flgConfirm then -- Prompt user to choose action flgConvert,flgConfirm = GUI_DataDialogue(ptrRecord,ptrGotTag,strNewTag) if not flgConfirm then if not flgConvert then break end -- Cancel all edits if intTagCount > 100 then -- Optionally start Progress Bar progbar.Start("Editing Tags",intTagCount) -- V3.8 end end end if flgConvert then -- Change/Delete the old Tag in the Record flgContinue = DoEditDataDetails(ptrRecord,ptrGotTag,strNewTag) -- V3.8 else -- Skip the old Tag in the Record WriteDataChange(ptrRecord,ptrGotTag,ptrNewTag,StrSkipTag) end end intTagCount = intTagCount - 1 -- Move onto next Tag progbar.Step(1) progbar.Message(strRecTag.." Rec Id "..fhGetRecordId(ptrRecord)) collectgarbage("step",0) -- Improves run time -- V3.4 end if progbar.Stop() then break end -- Cancel all edits? if not flgConfirm and not flgConvert then break end if not flgContinue then break end -- V3.8 end progbar.Close() local strOldData = strOldTag..": "..StrOldData local strSkipped = "" local strMessage = "" if intTagCount > 0 then -- Some items were skipped by cancelling IntSkippedData = IntSkippedData + intTagCount IntSkippedTotal = IntSkippedTotal + intTagCount strSkipped = "Edit cancelled and "..intTagCount.." '"..strOldData.."' items skipped." if intTagCount == 1 then strSkipped = strSkipped:gsub(" items "," item ") end WriteLog("\t"..strSkipped) strSkipped = strSkipped.."\n\n" end if IntSkippedData + IntDeletedData + IntChangedData == 0 then strMessage = "No '"..strOldData.."' items edited." else strMessage = "Finished '"..strOldData.."' item editing." end WriteLog("\t"..strMessage) iup_gui.CustomDialogue("Memo","0x0") -- Custom "Memo" dialogue minimum size & centralised iup_gui.MemoDialogue(strSkipped..strMessage.."\n"..UpdateCountStatus()) general.SaveStringToFile(table.concat(ArrLogFileText,"\r\n"),StrLogFilePath) -- Save Log File -- V3.7 if IntChangedData > 0 then -- Insert the New Tag into the Old Tag index and update dropdown lists DoInsertTagIndex(strRecTag,strNewSet,strNewTag,StrModeOld) StrPopulateDropdownLists(strRecTag) -- 17 Aug 2013 end if ( IntDeletedData + IntChangedData ) == intTagTotal then -- Remove the Old Tag from the Old Tag index and update dropdown lists DoRemoveTagIndex(strRecTag,strOldSet,strOldTag) StrPopulateDropdownLists(strRecTag) -- 17 Aug 2013 end end -- function DoChangeData -- Populate the Record Type dropdown list -- function StrPopulateRecordDropList() -- Preserve currently selected Record Type local strOldRec = iup.GetAttribute(LstRecords,tostring(LstRecords.Value)) local intOldRec = 0 LstRecords.RemoveItem = nil for intRecTag,strRecTag in ipairs(TblRecordTag) do if strRecTag ~= "INDI" and strRecTag ~= "FAM" then for intItem,strSet in ipairs(TblSetNameIndex[strRecTag]) do if FlgTableHasData(TblOldTagIndex[strRecTag][strSet]) then local strNewRec = TblRecordName[intRecTag] LstRecords.AppendItem = strNewRec -- Add the Record Type because it has associated Set names with associated Tags intOldRec = intOldRec + 1 if strNewRec == strOldRec then LstRecords.Value = intOldRec -- Select this Record Type if it was selected before end -- Works because TblRecordTag determines order in LstRecords which is not sorted break end end end end if LstRecords.Value == "0" then LstRecords.Value = 1 end return TblDictionary[iup.GetAttribute(LstRecords,tostring(LstRecords.Value))] end -- function StrPopulateRecordDropList -- Populate any pair of Tag Set dropdown lists -- function StrStrPopulateSetDropList(strRecTag,lstOldDrop,lstNewDrop) -- Preserve currently selected Set Names local strOldSet = iup.GetAttribute(lstOldDrop,tostring(lstOldDrop.Value)) local strNewSet = iup.GetAttribute(lstNewDrop,tostring(lstNewDrop.Value)) local intOldSet = 0 local intNewSet = 0 lstOldDrop.RemoveItem = nil lstNewDrop.RemoveItem = nil for intItem,strSet in ipairs(TblSetNameIndex[strRecTag]) do if FlgTableHasData(TblOldTagIndex[strRecTag][strSet]) then lstOldDrop.AppendItem = strSet -- Add the Set because it has associated Tags intOldSet = intOldSet + 1 if strSet == strOldSet then lstOldDrop.Value = intOldSet -- Select this Set if it was selected before end -- Works because TblSetNameIndex determines order in lstOldDrop which is not sorted end if FlgTableHasData(TblNewTagIndex[strRecTag][strSet]) and strSet ~= StrUDFTagsSet then lstNewDrop.AppendItem = strSet -- Add the Set because it has associated Tags, unless it is the UDF Tags Set intNewSet = intNewSet + 1 if strSet == strNewSet then lstNewDrop.Value = intNewSet -- Select this Set if it was selected before end -- Works because TblSetNameIndex determines order in lstNewDrop which is not sorted end end if lstOldDrop.Value == "0" then lstOldDrop.Value = 1 end if lstNewDrop.Value == "0" then lstNewDrop.Value = 1 end return iup.GetAttribute(lstOldDrop,tostring(lstOldDrop.Value)),iup.GetAttribute(lstNewDrop,tostring(lstNewDrop.Value)) end -- function StrStrPopulateSetDropList -- Populate any Tag Name dropdown list -- -- V3.7 function DoPopulateNameDropList(strRecTag,tblTagIndex,strSetName,lstDrop) -- Preserve currently selected Tag Name local strOldTag = iup.GetAttribute(lstDrop,tostring(lstDrop.Value)) local intOldTag = 0 lstDrop.RemoveItem = nil for intTag, strTag in ipairs(tblTagIndex[strSetName]) do local strNewTag = TblDictionary[strRecTag][strTag] lstDrop.AppendItem = strNewTag intOldTag = intOldTag + 1 end for intOldTag = 1, intOldTag do if strOldTag == iup.GetAttribute(lstDrop,tostring(intOldTag)) then lstDrop.Value = intOldTag -- Select this Tag Name if it was selected before break -- Need this technique because tblTagIndex is not same order as lstDrop sorted order end end -- Leave no Tag Name selected if nothing has matched end -- function DoPopulateNameDropList -- Populate the Dropdown lists on current Tab -- function StrPopulateDropdownLists(strRecTag) -- 17 Aug 2013 intChosenTab > strRecTag local strOldSet, strNewSet if strRecTag == "INDI" then -- Individual Records Tab -- Add each Fact Set name to Individual dropdown list strOldSet, strNewSet = StrStrPopulateSetDropList(strRecTag,LstIndOldSet,LstIndNewSet) -- Add each Fact Name to Individual dropdown lists DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strOldSet,LstIndOldTag) DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strNewSet,LstIndNewTag) elseif strRecTag == "FAM" then -- Family Records Tab -- Add each Fact Set name to Family dropdown list strOldSet, strNewSet = StrStrPopulateSetDropList(strRecTag,LstFamOldSet,LstFamNewSet) -- Add each Fact Name to Family dropdown lists DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strOldSet,LstFamOldTag) DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strNewSet,LstFamNewTag) elseif type(strRecTag) == "string" then -- Other Records Tab strRecTag = StrPopulateRecordDropList() -- Add each Tag Set name to Record dropdown lists strOldSet, strNewSet = StrStrPopulateSetDropList(strRecTag,LstRecOldSet,LstRecNewSet) -- Add each Tag Name to Record dropdown lists DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strOldSet,LstRecOldTag) DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strNewSet,LstRecNewTag) end return strRecTag end -- function StrPopulateDropdownLists -- GUI View Log File Dialogue -- function GUI_LogsDialogue(tblLogFile) -- Create each GUI text and button with title, etc local txtView = iup.text { ReadOnly="YES"; MultiLine="YES"; AutoHide="YES"; VisibleColumns="52"; VisibleLines="10"; } local btnOldest = iup.button { Title="Oldest Log File"; } local btnPrior = iup.button { Title="Prior Log File" ; } local btnDelete = iup.button { Title="Delete Log File"; } local btnNext = iup.button { Title="Next Log File" ; } local btnLatest = iup.button { Title="Latest Log File"; } local btnClose = iup.button { Title="Close View && Delete Log Files"; Expand="NO"; } -- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button local dialogLogs = iup.dialog { Title=StrPluginTitle.." View & Delete Log Files"; iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Margin; iup.frame { Font=iup_gui.FontHead; Title="View & Delete Log Files"; iup.vbox { txtView; iup.hbox { Expand="HORIZONTAL"; Homogeneous="YES"; btnOldest; btnPrior; btnDelete; btnNext; btnLatest; Margin=iup_gui.Margin; }; iup.hbox { Expand="HORIZONTAL"; Homogeneous="YES"; iup.fill{}; btnClose; iup.fill{}; }; }; }; }; } local intLogFile = tblLogFile[0] -- Number of log files local function doNothing() -- Inhibit general error message -- V3.7 end local function doDeleteLogFile() -- Action for Delete Log File button if not general.DeleteFile(tblLogFile[intLogFile],doNothing) then -- V3.7 iup.Message(StrPluginTitle, "Old log file not deleted!") else if tblLogFile[intLogFile] == StrLogFilePath then LblLogFile.Title = "Log File not created." -- Current log file deleted so update Main GUI LogFileHandle = nil end table.remove(tblLogFile,intLogFile) -- Remove log file name from table tblLogFile[0] = tblLogFile[0] - 1 -- Decrement number of log file names end end -- local function doDeleteLogFile local function doViewLogFile(strLogFile) -- Display Log File contents in multiline text box -- V3.7 txtView.Value = general.StrLoadFromFile(strLogFile) end -- local function doViewLogFile local function doUpdateDisplay() -- Update buttons and multiline text box if tblLogFile[0] <= 0 then tblLogFile[0] = 0 -- No log files exist, so all buttons inactive intLogFile = 0 btnOldest.Active = "NO" btnPrior.Active = "NO" btnDelete.Active = "NO" btnNext.Active = "NO" btnLatest.Active = "NO" txtView.Value = "No Log Files" elseif tblLogFile[0] == 1 then -- One log file exists, so only Delete button active intLogFile = 1 btnOldest.Active = "NO" btnPrior.Active = "NO" btnDelete.Active = "YES" btnNext.Active = "NO" btnLatest.Active = "NO" doViewLogFile(tblLogFile[intLogFile]) else btnOldest.Active = "YES" btnPrior.Active = "YES" btnDelete.Active = "YES" btnNext.Active = "YES" btnLatest.Active = "YES" if intLogFile == 1 then -- Oldest log file selected, so cannot go backward btnOldest.Active = "NO" btnPrior.Active = "NO" elseif intLogFile >= tblLogFile[0] then -- Latest log file selected, so cannot go forward intLogFile = tblLogFile[0] btnNext.Active = "NO" btnLatest.Active = "NO" end doViewLogFile(tblLogFile[intLogFile]) end end -- local function doUpdateDisplay -- Set other GUI control attributes local tblControls={ { "Font"; "FgColor"; "Tip"; "action"; {"TipBalloon";"Balloon";}; {"Expand";"YES";}; doUpdateDisplay; }; [txtView] = { "FontBody"; "Body"; "Selected log file details" ; false; }; [btnOldest] = { "FontBody"; "Safe"; "Select oldest log file" ; function() intLogFile=1 doUpdateDisplay() end; }; [btnPrior] = { "FontBody"; "Safe"; "Select prior log file" ; function() intLogFile=intLogFile-1 doUpdateDisplay() end; }; [btnDelete] = { "FontBody"; "Warn"; "Delete selected log file" ; function() doDeleteLogFile() doUpdateDisplay() end; }; [btnNext] = { "FontBody"; "Safe"; "Select next log file" ; function() intLogFile=intLogFile+1 doUpdateDisplay() end; }; [btnLatest] = { "FontBody"; "Safe"; "Select latest log file" ; function() intLogFile=tblLogFile[0] doUpdateDisplay() end; }; [btnClose] = { "FontBody"; "Risk"; "Close this View and Delete Log Files window"; function() return iup.CLOSE end; }; } iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes iup_gui.ShowDialogue("Logs",dialogLogs,btnClose,"normal keep") end -- function GUI_LogsDialogue -- GUI Warnings Dialogue -- function GUI_WarnDialogue(strName,strData,strWarn,intHelp) local isCont = true -- V3.8 local tblHelp = {"Check that the Source and Target are compatible";"Data value is logged in a Source Note";"Please report via FHUG Forum > Plugins Discussion";} -- Create each GUI label and button with title, etc local lblNameHead = iup.label { Title="Name: " ; Alignment="ARIGHT"; Size="50"; } local lblNameText = iup.label { Title=strName; } local lblDataHead = iup.label { Title="Data: " ; Alignment="ARIGHT"; Size="50"; } local lblDataText = iup.label { Title=strData; } local lblWarnHead = iup.label { Title="Warning: "; Alignment="ARIGHT"; Size="50"; } local lblWarnText = iup.label { Title=strWarn; } local lblHelpText = iup.label { Title=tblHelp[intHelp]; Alignment="ACENTER"; } local btnAbort = iup.button { Title="Abort Changes"; action=function() isCont = false return iup.CLOSE end; } -- V3.8 local btnClose = iup.button { Title="Close Warning"; action=function() isCont = true return iup.CLOSE end; } -- V3.8 -- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button local dialogWarn = iup.dialog { Title=StrPluginTitle.." Warning"; iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Border; iup.frame { Font=iup_gui.FontHead; FgColor=iup_gui.Head; Title="Warning"; iup.vbox { Margin=iup_gui.Margin; iup.hbox { lblNameHead; lblNameText; }; iup.hbox { lblDataHead; lblDataText; }; iup.hbox { lblWarnHead; lblWarnText; }; lblHelpText; iup.hbox { btnAbort; btnClose; }; -- V3.8 }; }; }; } -- Set other GUI control attributes local tblControls={ { "Font"; "FgColor"; "Expand"; "Tip"; {"TipBalloon";"Balloon";}; }; [lblNameHead] = { "FontBody"; "Body"; "VERTICAL" ; "Record name"; }; [lblNameText] = { "FontBody"; "Body"; "HORIZONTAL"; "Record name"; }; [lblDataHead] = { "FontBody"; "Body"; "VERTICAL" ; "Record data"; }; [lblDataText] = { "FontBody"; "Body"; "HORIZONTAL"; "Record data"; }; [lblWarnHead] = { "FontBody"; "Risk"; "VERTICAL" ; "Warning message"; }; [lblWarnText] = { "FontBody"; "Risk"; "HORIZONTAL"; "Warning message"; }; [lblHelpText] = { "FontBody"; "Safe"; "YES" ; "Advice message" ; }; [btnAbort] = { "FontBody"; "Risk"; "YES" ; "Abort all these Changes"; }; [btnClose] = { "FontBody"; "Risk"; "YES" ; "Close this Warning window"; }; } iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes iup_gui.ShowDialogue("Memo",dialogWarn,btnClose,"normal keep") return isCont -- V3.8 end -- function GUI_WarnDialogue -- GUI Data Dialogue -- function GUI_DataDialogue(ptrRecord,ptrOldData,strNewTag) local bEdit = false -- true means Edit Tags & false means Skip Tags local bConf = true -- true means confirm next Tag Edit & false means do the rest local strCol = "Body" -- Colour of lblEditHead/Text depends on strEditWarn local strEditText = "" -- Text statement of options local strEditWarn = "" -- Warning message makes text red local strEditData = "" -- Delete/Change this Tag button label local strEditRest = "" -- Delete/Change the Rest button label local strOldType = "" -- Source Tag type is "Event" or "Attribute" or "Tag" local strNewType = "" -- Target Tag type is "Event" or "Attribute" or "Tag" local ptrNewData = fhNewItemPtr() -- Determine if Fact Event or Fact Attribute or Data Tag -- local function strDataType(ptrData) local strType if fhIsAttribute(ptrData) then strType = "Attribute" elseif fhIsEvent(ptrData) then strType = "Event" elseif fhIsUDF(ptrData) then if StrOldData:sub(-5) == "Event" then strType = "Event" elseif StrOldData:sub(-9) == "Attribute" then strType = "Attribute" else strType = "Tag" end else strType = "Tag" end return strType end -- local function strDataType strOldType = strDataType(ptrOldData) if strNewTag == StrDeleteTag then strEditText = "Delete this "..strOldType.." ?"..strEditText strEditData = "Delete this "..strOldType strEditRest = "Delete all the Rest" else ptrNewData = fhCreateItem(strNewTag,ptrRecord) strNewType = strDataType(ptrNewData) fhDeleteItem(ptrNewData) if strOldType == "Attribute" and fhGetTag(ptrOldData) ~= "RESI" and ( strNewType == "Event" or strNewTag == "RESI" ) then strEditWarn = "\nAny Attribute value will migrate to a SOURce Note !" strCol = "Risk" end strEditText = "Change this "..strOldType.." to "..strNewTag..": "..StrNewData.." "..strNewType.." ?"..strEditWarn..strEditText strEditData = "Change to "..StrNewData.." "..strNewType strEditRest = "Change all the Rest" end -- Create each GUI label and button with title, etc local lblNameHead = iup.label { Title="Name: " ; Alignment="ARIGHT"; Size="50"; } local lblNameText = iup.label { Title=StrDisplayName(ptrRecord); } local lblDataHead = iup.label { Title="Data: " ; Alignment="ARIGHT"; Size="50"; } local lblDataText = iup.label { Title=StrDisplayData(ptrOldData); } local lblEditHead = iup.label { Title="Option: "; Alignment="ARIGHT"; Size="50"; } local lblEditText = iup.label { Title=strEditText; } local btnEditData = iup.button { Title=strEditData; } local btnSkipData = iup.button { Title="Skip this "..strOldType; } local btnEditRest = iup.button { Title=strEditRest; } local btnSkipRest = iup.button { Title="Skip all the Rest"; } -- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button local dialogData = iup.dialog { Title=StrPluginTitle.." Confirm Edit"; iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Border; iup.frame { Font=iup_gui.FontHead; FgColor=iup_gui.Head; Title="Confirm Edit"; iup.vbox { Margin=iup_gui.Margin; iup.hbox { lblNameHead; lblNameText; }; iup.hbox { lblDataHead; lblDataText; }; iup.hbox { lblEditHead; lblEditText; }; iup.hbox { btnEditData; btnSkipData; Homogeneous="YES"; }; iup.hbox { btnEditRest; btnSkipRest; Homogeneous="YES"; }; }; }; }; close_cb=function() bEdit=false bConf=false end; -- Skip all Tag changes } -- Set other GUI control attributes local tblControls={ { "Font"; "FgColor"; "Expand"; "Tip"; "action"; {"TipBalloon";"Balloon";}; }; [lblNameHead] = { "FontBody"; "Body"; "VERTICAL" ; "Fact/Tag name"; }; [lblNameText] = { "FontBody"; "Body"; "HORIZONTAL"; "Fact/Tag name"; }; [lblDataHead] = { "FontBody"; "Body"; "VERTICAL" ; "Fact/Tag data"; }; [lblDataText] = { "FontBody"; "Body"; "HORIZONTAL"; "Fact/Tag data"; }; [lblEditHead] = { "FontBody"; strCol; "VERTICAL" ; "Fact/Tag edit"; }; [lblEditText] = { "FontBody"; strCol; "HORIZONTAL"; "Fact/Tag edit"; }; [btnEditData] = { "FontBody"; "Safe"; "YES" ; "Edit this Fact/Tag" ; function() bEdit=true bConf=true return iup.CLOSE end; }; [btnSkipData] = { "FontBody"; "Safe"; "YES" ; "Skip this Fact/Tag" ; function() bEdit=false bConf=true return iup.CLOSE end; }; [btnEditRest] = { "FontBody"; "Safe"; "YES" ; "Edit rest of Facts/Tags"; function() bEdit=true bConf=false return iup.CLOSE end; }; [btnSkipRest] = { "FontBody"; "Safe"; "YES" ; "Skip rest of Facts/Tags"; function() bEdit=false bConf=false return iup.CLOSE end; }; } iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes iup_gui.ShowDialogue("Memo",dialogData,btnSkipRest,"normal keep") return bEdit,bConf end -- function GUI_DataDialogue -- GUI Main Dialogue -- function GUI_MainDialogue() local tblSet = TblSetNameIndex -- V3.7 local flgQuitMode = false -- btnQuit.action() = true, and btnClose.action() = false local strRecTag = "INDI" -- Current Record Tag "INDI", "FAM", "NOTE", "SOUR", etc local strIndOldSet = tblSet.INDI[1] -- Individual Old Tag Set chosen -- V3.7 local strIndOldTag = nil -- Individual Old Tag Name to be removed local strIndNewSet = tblSet.INDI[1] -- Individual New Tag Set chosen -- V3.7 local strIndNewTag = nil -- Individual New Tag Name to be inserted local tblIndFilter = {"";"";"";{};} -- Individual Old Tag Filters for Value, Date, Place, Records -- V2.9 local strFamOldSet = tblSet.FAM[1] -- Family Old Tag Set chosen -- V3.7 local strFamOldTag = nil -- Family Old Tag Name to be removed local strFamNewSet = tblSet.FAM[1] -- Family New Tag Set chosen -- V3.7 local strFamNewTag = nil -- Family New Tag Name to be inserted local tblFamFilter = {"";"";"";{};} -- Family Old Tag Filters for Value, Date, Place, Records -- V2.9 local strRecOldSet = tblSet.SOUR[1] -- Record Old Tag Set chosen -- V3.7 local strRecOldTag = nil -- Record Old Tag Name to be removed local strRecNewSet = tblSet.SOUR[1] -- Record New Tag Set chosen -- V3.7 local strRecNewTag = nil -- Record New Tag Name to be inserted local tblRecFilter = {"";"";"";{};} -- Record Old Tag Filters for Value, Date, Place, Records -- V2.9 local strIndDelete = " Delete every 'Source Tag Name' chosen above " local strIndChange = " Change 'Source Tag Name' into 'Target Tag Name' " local strFamDelete = " Delete every 'Source Tag Name' chosen above " local strFamChange = " Change 'Source Tag Name' into 'Target Tag Name' " local strRecDelete = " Delete every 'Source Tag Name' chosen above " local strRecChange = " Change 'Source Tag Name' into 'Target Tag Name' " -- If a button title is too long, then truncate long names and add elipsis local function strButtonTitle(strRecTag,strOldTag,strMid,strNewTag) local strOld = "" local strNew = "" if strOldTag then -- V2.5 strOld = TblDictionary[strRecTag][strOldTag] end if strNewTag then -- V2.5 strNew = TblDictionary[strRecTag][strNewTag] end if ( #strOld + #strMid + #strNew ) > IntMaxNameLen * 2 - 4 then if #strOld > IntMaxNameLen then strOld = strOld:sub(1,IntMaxNameLen-3).."..." end if #strNew > IntMaxNameLen then strNew = strNew:sub(1,IntMaxNameLen-3).."..." end end return strOld..strMid..strNew end -- local function strButtonTitle local function iupRadio(tglA,tglB) -- Return radio handle created from two toggles return iup.radio { iup.hbox { tglA; tglB; Homogeneous="YES"; }; Value=tglA; } end -- local function iupRadio -- Create the Individual Records controls with titles/values and tooltips, etc local btnCHR_BAPM = iup.button { Title=strButtonTitle("INDI","CHR"," to ","BAPM"); } local btnBAPM_CHR = iup.button { Title=strButtonTitle("INDI","BAPM"," to ","CHR"); } local lblIndOldSet = iup.label { Title=" Source Tag Set:" ; Alignment="ARIGHT"; Size="80"; } LstIndOldSet = iup.list { DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; } local lblIndNewSet = iup.label { Title=" Target Tag Set:" ; Alignment="ARIGHT"; Size="80"; } LstIndNewSet = iup.list { DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; } local lblIndOldTag = iup.label { Title=" Source Tag Name:"; Alignment="ARIGHT"; Size="80"; } LstIndOldTag = iup.list { DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; } local lblIndNewTag = iup.label { Title=" Target Tag Name:"; Alignment="ARIGHT"; Size="80"; } LstIndNewTag = iup.list { DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; } local btnIndDelete = iup.button { Title=strIndDelete; Active="NO"; } local btnIndChange = iup.button { Title=strIndChange; Active="NO"; } local lblIndValue = iup.label { Title=" Value:"; } local txtIndValue = iup.text { Value=tblIndFilter[IntFilterValue]; } local lblIndDate = iup.label { Title=" Date:"; } local txtIndDate = iup.text { Value=tblIndFilter[IntFilterDate]; } local lblIndPlace = iup.label { Title=" Place:"; } local txtIndPlace = iup.text { Value=tblIndFilter[IntFilterPlace]; } local lblIndRecord = iup.label { Title=" Records: "; } local tglIndRecAll = iup.toggle { Title="All"; } -- V2.9 local tglIndRecSel = iup.toggle { Title="Pick"; } -- V2.9 local radIndRecord = iupRadio ( tglIndRecAll, tglIndRecSel ) -- V2.9 -- Create the Family Records controls with titles/values and tooltips, etc local btnDIV_ANUL = iup.button { Title=strButtonTitle("FAM","DIV"," to ","ANUL"); } local btnANUL_DIV = iup.button { Title=strButtonTitle("FAM","ANUL"," to ","DIV"); } local lblFamOldSet = iup.label { Title=" Source Tag Set:" ; Alignment="ARIGHT"; Size="80"; } LstFamOldSet = iup.list { DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; } local lblFamNewSet = iup.label { Title=" Target Tag Set:" ; Alignment="ARIGHT"; Size="80"; } LstFamNewSet = iup.list { DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; } local lblFamOldTag = iup.label { Title=" Source Tag Name:"; Alignment="ARIGHT"; Size="80"; } LstFamOldTag = iup.list { DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; } local lblFamNewTag = iup.label { Title=" Target Tag Name:"; Alignment="ARIGHT"; Size="80"; } LstFamNewTag = iup.list { DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; } local btnFamDelete = iup.button { Title=strFamDelete; Active="NO"; } local btnFamChange = iup.button { Title=strFamChange; Active="NO"; } local lblFamValue = iup.label { Title=" Value:"; } local txtFamValue = iup.text { Value=tblFamFilter[IntFilterValue]; } local lblFamDate = iup.label { Title=" Date:"; } local txtFamDate = iup.text { Value=tblFamFilter[IntFilterDate]; } local lblFamPlace = iup.label { Title=" Place:"; } local txtFamPlace = iup.text { Value=tblFamFilter[IntFilterPlace]; } local lblFamRecord = iup.label { Title=" Records: "; } local tglFamRecAll = iup.toggle { Title="All"; } -- V2.9 local tglFamRecSel = iup.toggle { Title="Pick"; } -- V2.9 local radFamRecord = iupRadio ( tglFamRecAll, tglFamRecSel ) -- V2.9 -- Create the Other Records controls with titles/values and tooltips, etc local lblRecords = iup.label { Title=" Record Type:" ; Alignment="ARIGHT"; Size="80"; } LstRecords = iup.list { DropDown="YES"; Value="1" ; VisibleColumns="9"; Visible_Items="16"; } local lblRecOldSet = iup.label { Title=" Source Tag Set:" ; Alignment="ARIGHT"; Size="80"; } LstRecOldSet = iup.list { DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; } local lblRecNewSet = iup.label { Title=" Target Tag Set:" ; Alignment="ARIGHT"; Size="80"; } LstRecNewSet = iup.list { DropDown="YES"; Sort="NO" ; VisibleColumns="9"; Visible_Items="16"; } local lblRecOldTag = iup.label { Title=" Source Tag Name:"; Alignment="ARIGHT"; Size="80"; } LstRecOldTag = iup.list { DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; } local lblRecNewTag = iup.label { Title=" Target Tag Name:"; Alignment="ARIGHT"; Size="80"; } LstRecNewTag = iup.list { DropDown="YES"; Sort="YES"; VisibleColumns="9"; Visible_Items="16"; } local btnRecDelete = iup.button { Title=strRecDelete; Active="NO"; } local btnRecChange = iup.button { Title=strRecChange; Active="NO"; } local lblRecValue = iup.label { Title=" Value:"; } local txtRecValue = iup.text { Value=tblRecFilter[IntFilterValue]; } local lblRecDate = iup.label { Title=" Date:"; } local txtRecDate = iup.text { Value=tblRecFilter[IntFilterDate]; } local lblRecPlace = iup.label { Title=" Place:"; } local txtRecPlace = iup.text { Value=tblRecFilter[IntFilterPlace]; } local lblRecRecord = iup.label { Title=" Records: "; } local tglRecRecAll = iup.toggle { Title="All"; } -- V2.9 local tglRecRecSel = iup.toggle { Title="Pick"; } -- V2.9 local radRecRecord = iupRadio ( tglRecRecAll, tglRecRecSel ) -- V2.9 -- Create the Status & Settings controls with titles/values and tooltips, etc local lblCounts = iup.label { Title=" Data Counts: "; Alignment="ARIGHT"; } LblCoSkipped = iup.label { Title=" 0 Skipped. "; Alignment="ARIGHT"; } LblCoDeleted = iup.label { Title=" 0 Deleted. "; Alignment="ARIGHT"; } LblCoChanged = iup.label { Title=" 0 Changed. "; Alignment="ARIGHT:ABOTTOM"; } LblCoWarning = iup.label { Title=" 0 Warnings."; Alignment="ARIGHT"; } local lblTotals = iup.label { Title=" Data Totals: "; Alignment="ARIGHT"; } LblToSkipped = iup.label { Title=" 0 Skipped. "; Alignment="ARIGHT"; } LblToDeleted = iup.label { Title=" 0 Deleted. "; Alignment="ARIGHT"; } LblToChanged = iup.label { Title=" 0 Changed. "; Alignment="ARIGHT"; } LblToWarning = iup.label { Title=" 0 Warnings."; Alignment="ARIGHT"; } local tglLogNeed = iup.toggle { Title=StrLogNeedTitle.." ?"; } local tglPattern = iup.toggle { Title=StrPatternTitle.." ?"; } local tglConfirm = iup.toggle { Title=StrConfirmTitle.." ?"; } local tglWarning = iup.toggle { Title=StrWarningTitle.." ?"; } LblLogFile = iup.label { Title="Log File not created."; WordWrap="YES"; Alignment=":ATOP"; Size="90x16"; } local btnViewLog = iup.button { Title=" View && Delete Log Files" } local btnUpdates = iup.button { Title="Check for Updates"; } -- V4.0 local btnDefault = iup.button { Title="Restore Default Settings" } local btnSetFont = iup.button { Title="Set Window Fonts" } local btnQuit = iup.button { Title="QUIT Plugin and UNDO Updates" } local btnGetHelp = iup.button { Title=" Obtain Help && Advice" } local btnClose = iup.button { Title="CLOSE Plugin and KEEP Updates"} local intTabPosn = 0 -- GUI Tab currently displayed, 0=Undefined, 1=Individual, 2=Family, 3=Other, 4=Status -- Create the Individual Records box local headIndiv = iup.label { Title=" ~ Choose an Individual Record Fact/Tag to Edit ~ "; } local vboxIndiv = iup.vbox { Alignment="ACENTER"; Margin="0x0"; headIndiv; iup.hbox { Homogeneous="YES"; iup.hbox { iup.label{Size="80";};btnCHR_BAPM;} ; iup.hbox { iup.label{Size="80";};btnBAPM_CHR;}; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblIndOldSet; LstIndOldSet; } ; iup.hbox { lblIndNewSet; LstIndNewSet; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblIndOldTag; LstIndOldTag; } ; iup.hbox { lblIndNewTag; LstIndNewTag; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { btnIndDelete; } ; iup.hbox { btnIndChange; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblIndValue; txtIndValue; } ; iup.hbox { lblIndDate; txtIndDate; }; iup.hbox { lblIndPlace; txtIndPlace; }; iup.hbox { lblIndRecord; radIndRecord; }; }; -- V2.9 } -- Create the Family Records box local headFamily = iup.label { Title=" ~ Choose a Family Record Fact/Tag to Edit ~ "; } local vboxFamily = iup.vbox { Alignment="ACENTER"; Margin="0x0"; headFamily; iup.hbox { Homogeneous="YES"; iup.hbox { iup.label{Size="80";};btnDIV_ANUL;} ; iup.hbox { iup.label{Size="80";};btnANUL_DIV; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblFamOldSet; LstFamOldSet; } ; iup.hbox { lblFamNewSet; LstFamNewSet; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblFamOldTag; LstFamOldTag; } ; iup.hbox { lblFamNewTag; LstFamNewTag; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { btnFamDelete; } ; iup.hbox { btnFamChange; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblFamValue; txtFamValue; } ; iup.hbox { lblFamDate; txtFamDate; }; iup.hbox { lblFamPlace; txtFamPlace; }; iup.hbox { lblFamRecord; radFamRecord; }; }; -- V2.9 } -- Create the Other Records box local headRecord = iup.label { Title=" ~ Choose a Record Type Gedcom Tag to Edit ~ "; } local vboxRecord = iup.vbox { Alignment="ACENTER"; Margin="0x0"; headRecord; iup.hbox { Homogeneous="YES"; iup.hbox { lblRecords ; LstRecords; } ; iup.hbox { }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblRecOldSet; LstRecOldSet; } ; iup.hbox { lblRecNewSet; LstRecNewSet; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblRecOldTag; LstRecOldTag; } ; iup.hbox { lblRecNewTag; LstRecNewTag; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { btnRecDelete; } ; iup.hbox { btnRecChange; }; }; iup.hbox { Homogeneous="YES"; iup.hbox { lblRecValue; txtRecValue; } ; iup.hbox { lblRecDate; txtRecDate; }; iup.hbox { lblRecPlace; txtRecPlace; }; iup.hbox { lblRecRecord; radRecRecord; }; }; -- V2.9 } -- Create the Status box local headStatus = iup.label { Title=" ~ Status and Settings Information ~ "; } HboxCounts = iup.hbox { Homogeneous="YES"; lblCounts; LblCoSkipped; LblCoDeleted; LblCoChanged; LblCoWarning; iup.label { }; } local hboxTotals = iup.hbox { Homogeneous="YES"; lblTotals; LblToSkipped; LblToDeleted; LblToChanged; LblToWarning; iup.label {visible="NO";}; } local vboxStatus = iup.vbox { Alignment="ACENTER"; Margin="2x0"; headStatus; iup.vbox { Homogeneous="YES"; HboxCounts; hboxTotals; iup.hbox { Homogeneous="YES"; tglLogNeed; tglPattern; tglConfirm; tglWarning; }; }; iup.hbox { Homogeneous="YES"; LblLogFile; }; iup.hbox { Homogeneous="YES"; btnViewLog; btnUpdates; btnDefault; btnSetFont; Margin="0x0"; }; -- V4.0 } -- Create the Tab controls local tabControls = iup.tabs { vboxIndiv; TabTitle0="Individual Records"; vboxFamily; TabTitle1="Family Records"; vboxRecord; TabTitle2="Other Records"; vboxStatus; TabTitle3="Status && Settings "; tabchangepos_cb=function(self,intNewTab,intOldTab) intTabPosn=intNewTab+1 if intTabPosn <= 3 then strRecTag=StrPopulateDropdownLists(TblRecordTag[intTabPosn]) end end; -- 17 Aug 2013 -- 28 Dec 2013 } -- Create dialogue and turn off resize, menubox, maximize and minimize local headMain = iup.label { Alignment="ACENTER:ACENTER"; Title="Please only use this plugin on a copy of your data until you are satisfied with the results."; } local vboxMain = iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Margin; headMain; tabControls; iup.hbox { Homogeneous="YES"; btnQuit; btnGetHelp; btnClose; }; } local dialogMain = iup.dialog { Title=StrPluginTitle..iup_gui.Version; vboxMain; close_cb=function() if vboxMain.Active == "NO" then return iup.IGNORE else return iup.CLOSE end end; } if fhGetAppVersion() > 6 then -- FH V7 IUP 3.28 -- V3.1 tabControls.TabPadding="10x7" else -- FH V6 IUP 3.11 -- V3.1 tabControls.Padding="10x7" end local function doChangeData(...) -- Disable GUI while changing data vboxMain.Active = "NO" DoChangeData(...) vboxMain.Active = "YES" end -- local function doChangeData local function setControls() -- Set GUI control values tglLogNeed.Value = StrLogNeed tglPattern.Value = StrPattern tglConfirm.Value = StrConfirm tglWarning.Value = StrWarning tblIndFilter[IntFilterRecord] = {} -- Reset the Records filters -- V2.9 tglIndRecAll.Value = "ON" tblFamFilter[IntFilterRecord] = {} tglFamRecAll.Value = "ON" tblRecFilter[IntFilterRecord] = {} tglRecRecAll.Value = "ON" end -- local function setControls local function setToolTip(iupItem) -- Refresh control tooltip otherwise it vanishes in XP! iupItem.Tip = iupItem.Tip end -- local function setToolTip local strTipDelete = "Delete selected Source Facts/Tags" local strTipChange = "Change selected Source Facts/Tags to Target Facts/Tags" local strTipOldSet = "Select source Fact/Tag Set for the list below" local strTipNewSet = "Select target Fact/Tag Set for the list below" local strTipOldTag = "Select source Fact/Tag Name from the Set above" local strTipNewTag = "Select target Fact/Tag Name from the Set above" local strTipValue = "Filter source on Attribute Value" local strTipDate = "Filter source on Fact/Tag Date" local strTipPlace = "Filter source on Fact/Tag Place" local strTipRecord = "Filter source by Records ?" -- V2.9 local strTipRecAll = "Do not filter source by Records" -- V2.9 local strTipRecSel = "Filter source by selected Records" -- V2.9 local tblState = { "OFF"; "ON"; } -- Toggle control settings per state -- Set other GUI control attributes, but see later for further action functions local tblControls={{ "Font"; "FgColor"; "Expand"; "Tip"; "action"; {"TipBalloon";"Balloon"}; setControls; }; -- Individual Records [headIndiv] = { "FontHead"; "Head"; "VERTICAL"; }; [vboxIndiv] = { "FontBody"; "Body"; false; }; [btnCHR_BAPM] = { "FontBody"; "Safe"; "YES"; "Change Christening to Baptism Events"; function() doChangeData("INDI",StrStandEvents,StrStandEvents,"CHR","BAPM",tblIndFilter) end; }; [btnBAPM_CHR] = { "FontBody"; "Safe"; "YES"; "Change Baptism to Christening Events"; function() doChangeData("INDI",StrStandEvents,StrStandEvents,"BAPM","CHR",tblIndFilter) end; }; [lblIndOldSet]= { "FontBody"; "Body"; false; strTipOldSet; }; [LstIndOldSet]= { "FontBody"; "Safe"; "YES"; strTipOldSet; }; [lblIndNewSet]= { "FontBody"; "Body"; false; strTipNewSet; }; [LstIndNewSet]= { "FontBody"; "Safe"; "YES"; strTipNewSet; }; [lblIndOldTag]= { "FontBody"; "Body"; false; strTipOldTag; }; [LstIndOldTag]= { "FontBody"; "Safe"; "YES"; strTipOldTag; }; [lblIndNewTag]= { "FontBody"; "Body"; false; strTipNewTag; }; [LstIndNewTag]= { "FontBody"; "Safe"; "YES"; strTipNewTag; }; [btnIndDelete]= { "FontBody"; "Safe"; "YES"; strTipDelete; }; [btnIndChange]= { "FontBody"; "Safe"; "YES"; strTipChange; }; [lblIndValue] = { "FontBody"; "Body"; false; strTipValue ; }; [txtIndValue] = { "FontBody"; "Safe"; "YES"; strTipValue ; }; [lblIndDate] = { "FontBody"; "Body"; false; strTipDate ; }; [txtIndDate] = { "FontBody"; "Safe"; "YES"; strTipDate ; }; [lblIndPlace] = { "FontBody"; "Body"; false; strTipPlace ; }; [txtIndPlace] = { "FontBody"; "Safe"; "YES"; strTipPlace ; }; [lblIndRecord]= { "FontBody"; "Body"; false; strTipRecord; }; -- V2.9 [tglIndRecAll]= { "FontBody"; "Body"; "YES"; strTipRecAll; }; -- V2.9 [tglIndRecSel]= { "FontBody"; "Body"; "YES"; strTipRecSel; }; -- V2.9 -- Family Records [headFamily] = { "FontHead"; "Head"; "VERTICAL"; }; [vboxFamily] = { "FontBody"; "Body"; false; }; [btnDIV_ANUL] = { "FontBody"; "Safe"; "YES"; "Change Divorce to Annulment Events"; function() doChangeData("FAM",StrStandEvents,StrStandEvents,"DIV","ANUL",tblFamFilter) end; }; [btnANUL_DIV] = { "FontBody"; "Safe"; "YES"; "Change Annulment to Divorce Events"; function() doChangeData("FAM",StrStandEvents,StrStandEvents,"ANUL","DIV",tblFamFilter) end; }; [lblFamOldSet]= { "FontBody"; "Body"; false; strTipOldSet; }; [LstFamOldSet]= { "FontBody"; "Safe"; "YES"; strTipOldSet; }; [lblFamNewSet]= { "FontBody"; "Body"; false; strTipNewSet; }; [LstFamNewSet]= { "FontBody"; "Safe"; "YES"; strTipNewSet; }; [lblFamOldTag]= { "FontBody"; "Body"; false; strTipOldTag; }; [LstFamOldTag]= { "FontBody"; "Safe"; "YES"; strTipOldTag; }; [lblFamNewTag]= { "FontBody"; "Body"; false; strTipNewTag; }; [LstFamNewTag]= { "FontBody"; "Safe"; "YES"; strTipNewTag; }; [btnFamDelete]= { "FontBody"; "Safe"; "YES"; strTipDelete; }; [btnFamChange]= { "FontBody"; "Safe"; "YES"; strTipChange; }; [lblFamValue] = { "FontBody"; "Body"; false; strTipValue ; }; [txtFamValue] = { "FontBody"; "Safe"; "YES"; strTipValue ; }; [lblFamDate] = { "FontBody"; "Body"; false; strTipDate ; }; [txtFamDate] = { "FontBody"; "Safe"; "YES"; strTipDate ; }; [lblFamPlace] = { "FontBody"; "Body"; false; strTipPlace ; }; [txtFamPlace] = { "FontBody"; "Safe"; "YES"; strTipPlace ; }; [lblFamRecord]= { "FontBody"; "Body"; false; strTipRecord; }; -- V2.9 [tglFamRecAll]= { "FontBody"; "Body"; "YES"; strTipRecAll; }; -- V2.9 [tglFamRecSel]= { "FontBody"; "Body"; "YES"; strTipRecSel; }; -- V2.9 -- Other Records [headRecord] = { "FontHead"; "Head"; "VERTICAL"; }; [vboxRecord] = { "FontBody"; "Body"; false; }; [lblRecords] = { "FontBody"; "Body"; false; "Select record type"; }; [LstRecords] = { "FontBody"; "Safe"; "YES"; "Select record type"; }; [lblRecOldSet]= { "FontBody"; "Body"; false; strTipOldSet:gsub("Fact/",""); }; [LstRecOldSet]= { "FontBody"; "Safe"; "YES"; strTipOldSet:gsub("Fact/",""); }; [lblRecNewSet]= { "FontBody"; "Body"; false; strTipNewSet:gsub("Fact/",""); }; [LstRecNewSet]= { "FontBody"; "Safe"; "YES"; strTipNewSet:gsub("Fact/",""); }; [lblRecOldTag]= { "FontBody"; "Body"; false; strTipOldTag:gsub("Fact/",""); }; [LstRecOldTag]= { "FontBody"; "Safe"; "YES"; strTipOldTag:gsub("Fact/",""); }; [lblRecNewTag]= { "FontBody"; "Body"; false; strTipNewTag:gsub("Fact/",""); }; [LstRecNewTag]= { "FontBody"; "Safe"; "YES"; strTipNewTag:gsub("Fact/",""); }; [btnRecDelete]= { "FontBody"; "Safe"; "YES"; strTipDelete:gsub("Facts/",""); }; [btnRecChange]= { "FontBody"; "Safe"; "YES"; strTipChange:gsub("Facts/",""); }; [lblRecValue] = { "FontBody"; "Body"; false; strTipValue:gsub("Attribute","Tag"); }; [txtRecValue] = { "FontBody"; "Safe"; "YES"; strTipValue:gsub("Attribute","Tag"); }; [lblRecDate] = { "FontBody"; "Body"; false; strTipDate :gsub("Fact/",""); }; [txtRecDate] = { "FontBody"; "Safe"; "YES"; strTipDate :gsub("Fact/",""); }; [lblRecPlace] = { "FontBody"; "Body"; false; strTipPlace:gsub("Fact/",""); }; [txtRecPlace] = { "FontBody"; "Safe"; "YES"; strTipPlace:gsub("Fact/",""); }; [lblRecRecord]= { "FontBody"; "Body"; false; strTipRecord; }; -- V2.9 [tglRecRecAll]= { "FontBody"; "Body"; "YES"; strTipRecAll; }; -- V2.9 [tglRecRecSel]= { "FontBody"; "Body"; "YES"; strTipRecSel; }; -- V2.9 -- Status & Settings [headStatus] = { "FontHead"; "Head"; "VERTICAL"; }; [vboxStatus] = { "FontBody"; "Body"; false; }; [lblCounts] = { "FontBody"; "Body"; "YES"; "Counts for the last edit"; }; [LblCoSkipped]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags skipped by last edit"; }; [LblCoDeleted]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags deleted by last edit"; }; [LblCoChanged]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags changed by last edit"; }; [LblCoWarning]= { "FontBody"; "Body"; "YES"; "Number of Fact/Tag warnings for last edit"; }; [lblTotals] = { "FontBody"; "Body"; "YES"; "Totals for this session"; }; [LblToSkipped]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags skipped this session"; }; [LblToDeleted]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags deleted this session"; }; [LblToChanged]= { "FontBody"; "Body"; "YES"; "Number of Facts/Tags changed this session"; }; [LblToWarning]= { "FontBody"; "Body"; "YES"; "Number of Fact/Tag warnings this session"; }; [tglLogNeed] = { "FontBody"; "Body"; "YES"; "Choose whether to log edits to file" ; function(self,intState) StrLogNeed = tblState[intState+1] SaveSettings() setToolTip(self) end; }; -- Refresh XP Tooltips -- V2.4 [tglPattern] = { "FontBody"; "Body"; "YES"; "Choose to Filter with Regular Expression style Patterns, or Plain Text" ; function(self,intState) StrPattern = tblState[intState+1] SaveSettings() setToolTip(self) end; }; [tglConfirm] = { "FontBody"; "Body"; "YES"; "Choose whether to confirm Fact/Tag edits" ; function(self,intState) StrConfirm = tblState[intState+1] SaveSettings() setToolTip(self) end; }; [tglWarning] = { "FontBody"; "Body"; "YES"; "Choose whether to show warning messages" ; function(self,intState) StrWarning = tblState[intState+1] SaveSettings() setToolTip(self) end; }; [LblLogFile] = { "FontBody"; "Body"; "YES"; "Log file name (when created)"; }; [btnViewLog] = { "FontBody"; "Safe"; "YES"; "View and optionally delete all log files"; }; [btnUpdates] = { "FontBody"; "Safe"; "YES"; "Check for a later plugin version in the Plugin Store"; }; -- V4.0 [btnDefault] = { "FontBody"; "Safe"; "YES"; "Restore default Settings for Window sizes and positions, \n"..StrLogNeedTitle..", "..StrPatternTitle..", "..StrConfirmTitle..", and "..StrWarningTitle; }; [btnSetFont] = { "FontBody"; "Safe"; "YES"; "Choose user interface window font styles"; }; -- Main Controls [headMain] = { "FontHead"; "Risk"; "VERTICAL"; }; [vboxMain] = { "FontHead"; "Head"; false; }; [tabControls] = { "FontHead"; "Head"; false; "Choose between Individual Records, Family Records, or \nOther Records such as Note, Source, Repository, etc, \nor Status and Settings Information."; }; [btnQuit] = { "FontBody"; "Risk"; "YES"; "Quit the plugin and undo any updates" ; function() flgQuitMode=true return iup.CLOSE end; }; [btnGetHelp] = { "FontBody"; "Safe"; "YES"; "Obtain online Help and Advice from the Plugin Store"; }; [btnClose] = { "FontBody"; "Risk"; "YES"; "Close the plugin and keep all updates" ; function() return iup.CLOSE end; }; } -- GUI Control Functions -- local function doUpdateButtons(strRecord,strOldTag,strNewTag,btnChange,strChange,btnDelete) if strOldTag and strNewTag and strNewTag ~= strOldTag then btnChange.Active = "YES" -- Enable & label Change button if Source & Target Tag Names OK btnChange.Title = strButtonTitle(strRecord,strOldTag," to ",strNewTag) else btnChange.Active = "NO" -- Disable & label Change button btnChange.Title = strChange end if btnDelete and strOldTag then btnDelete.Active = "YES" -- Enable & label Delete button if Source Tag Name OK btnDelete.Title = strButtonTitle(strRecord,nil,"Delete every ",strOldTag) end end -- local function doUpdateButtons local function doIndButtonsReset(strMode) -- Reset the Delete & Change buttons and Tag Name lists -- strMode = StrModeAll if either button is pressed -- strMode = StrModeOld if 'Source Tag Set' is changed -- strMode = StrModeNew if 'Target Tag Set' is changed btnIndChange.Title = strIndChange btnIndChange.Active = "NO" if strMode == StrModeOld or strMode == StrModeAll then btnIndDelete.Title = strIndDelete btnIndDelete.Active = "NO" LstIndOldTag.Value = 0 strIndOldTag = nil end if strMode == StrModeNew or strMode == StrModeAll then LstIndNewTag.Value = 0 strIndNewTag = nil end end -- local function doIndButtonsReset local function getRecordFilter(strType) -- Prompt user for Record selection -- V2.9 dialogMain.Active="No" -- V3.8 local arrRec = fhPromptUserForRecordSel(strType) if #arrRec == 0 then -- Cancelled so use current selection arrRec = fhGetCurrentRecordSel(strType) end dialogMain.Active="Yes" -- V3.8 return arrRec end -- local function getRecordFilter function btnIndDelete:action() -- Action for Delete fact button doChangeData("INDI",strIndOldSet,"",strIndOldTag,StrDeleteTag,tblIndFilter) doIndButtonsReset(StrModeAll) end function btnIndChange:action() -- Action for Change facts button doChangeData("INDI",strIndOldSet,strIndNewSet,strIndOldTag,strIndNewTag,tblIndFilter) doIndButtonsReset(StrModeAll) end function LstIndOldSet:action(strText,intItem,iState) -- Action for Source Tag Set dropdown if iState == 1 then setToolTip(LstIndOldSet) -- Refresh XP Tooltip -- V2.4 LstIndOldSet.Value = intItem local indoldsetval = LstIndOldSet.Value -- Populate Source Tag Name dropdown strIndOldSet = strText DoPopulateNameDropList(strRecTag,TblOldTagIndex["INDI"],strIndOldSet,LstIndOldTag) doIndButtonsReset(StrModeOld) end end function LstIndNewSet:action(strText,intItem,iState) -- Action for Target Tag Set dropdown if iState == 1 then setToolTip(LstIndNewSet) -- Refresh XP Tooltip -- V2.4 LstIndNewSet.Value = intItem local indnewsetval = LstIndNewSet.Value -- Populate Target Tag Name dropdown strIndNewSet = strText DoPopulateNameDropList(strRecTag,TblNewTagIndex["INDI"],strIndNewSet,LstIndNewTag) doIndButtonsReset(StrModeNew) end end -- function LstIndNewSet:action function LstIndOldTag:action(strText,intItem,iState) -- Action for Source Tag Name dropdown if iState == 1 then setToolTip(LstIndOldTag) -- Refresh XP Tooltip -- V2.4 strIndOldTag = TblDictionary["INDI"][strIndOldSet][strText] -- V2.5 doUpdateButtons("INDI",strIndOldTag,strIndNewTag,btnIndChange,strIndChange,btnIndDelete) end end function LstIndNewTag:action(strText,intItem,iState) -- Action for Target Tag Name dropdown if iState == 1 then setToolTip(LstIndNewTag) -- Refresh XP Tooltip -- V2.4 strIndNewTag = TblDictionary["INDI"][strIndNewSet][strText] -- V2.5 doUpdateButtons("INDI",strIndOldTag,strIndNewTag,btnIndChange,strIndChange) end end function txtIndValue:valuechanged_cb() -- Call back when Filter Value is changed tblIndFilter[IntFilterValue] = txtIndValue.Value end function txtIndDate:valuechanged_cb() -- Call back when Filter Date is changed tblIndFilter[IntFilterDate] = txtIndDate.Value end function txtIndPlace:valuechanged_cb() -- Call back when Filter Place is changed tblIndFilter[IntFilterPlace] = txtIndPlace.Value end function tglIndRecAll:action(intState) -- Action for Record All toggle -- V2.9 if intState == 1 then setToolTip(tglIndRecAll) -- Refresh XP Tooltip tblIndFilter[IntFilterRecord] = {} end end function tglIndRecSel:action(intState) -- Action for Record Pick toggle -- V2.9 if intState == 1 then setToolTip(tglIndRecSel) -- Refresh XP Tooltip tblIndFilter[IntFilterRecord] = getRecordFilter("INDI") end end local function doFamButtonsReset(strMode) -- Reset the Delete & Change buttons and Tag Name lists -- strMode = StrModeAll if either button is used -- strMode = StrModeOld if 'Source Tag Set' is changed -- strMode = StrModeNew if 'Target Tag Set' is changed btnFamChange.Title = strFamChange btnFamChange.Active = "NO" if strMode == StrModeOld or strMode == StrModeAll then btnFamDelete.Title = strFamDelete btnFamDelete.Active = "NO" LstFamOldTag.Value = 0 strFamOldTag = nil end if strMode == StrModeNew or strMode == StrModeAll then LstFamNewTag.Value = 0 strFamNewTag = nil end end -- local function doFamButtonsReset function btnFamDelete:action() -- Action for Delete fact button doChangeData("FAM",strFamOldSet,"",strFamOldTag,StrDeleteTag,tblFamFilter) doFamButtonsReset(StrModeAll) end function btnFamChange:action() -- Action for Change facts button doChangeData("FAM",strFamOldSet,strFamNewSet,strFamOldTag,strFamNewTag,tblFamFilter) doFamButtonsReset(StrModeAll) end function LstFamOldSet:action(strText,intItem,iState) -- Action for Source Tag Set dropdown if iState == 1 then setToolTip(LstFamOldSet) -- Refresh XP Tooltip -- V2.4 strFamOldSet = strText -- Populate Source Tag Name dropdown DoPopulateNameDropList(strRecTag,TblOldTagIndex["FAM"],strFamOldSet,LstFamOldTag) doFamButtonsReset(StrModeOld) end end function LstFamNewSet:action(strText,intItem,iState) -- Action for Target Tag Set dropdown if iState == 1 then setToolTip(LstFamNewSet) -- Refresh XP Tooltip -- V2.4 strFamNewSet = strText -- Populate Target Tag Name dropdown DoPopulateNameDropList(strRecTag,TblNewTagIndex["FAM"],strFamNewSet,LstFamNewTag) doFamButtonsReset(StrModeNew) end end function LstFamOldTag:action(strText,intItem,iState) -- Action for Source Tag Name dropdown if iState == 1 then setToolTip(LstFamOldTag) -- Refresh XP Tooltip -- V2.4 strFamOldTag = TblDictionary["FAM"][strFamOldSet][strText] -- V2.5 doUpdateButtons("FAM",strFamOldTag,strFamNewTag,btnFamChange,strFamChange,btnFamDelete) end end function LstFamNewTag:action(strText,intItem,iState) -- Action for Target Tag Name dropdown if iState == 1 then setToolTip(LstFamNewTag) -- Refresh XP Tooltip -- V2.4 strFamNewTag = TblDictionary["FAM"][strFamNewSet][strText] -- V2.5 doUpdateButtons("FAM",strFamOldTag,strFamNewTag,btnFamChange,strFamChange) end end function txtFamValue:valuechanged_cb() -- Call back when Filter Value is changed tblFamFilter[IntFilterValue] = txtFamValue.Value end function txtFamDate:valuechanged_cb() -- Call back when Filter Date is changed tblFamFilter[IntFilterDate] = txtFamDate.Value end function txtFamPlace:valuechanged_cb() -- Call back when Filter Place is changed tblFamFilter[IntFilterPlace] = txtFamPlace.Value end function tglFamRecAll:action(intState) -- Action for Record All toggle -- V2.9 if intState == 1 then setToolTip(tglFamRecAll) -- Refresh XP Tooltip tblFamFilter[IntFilterRecord] = {} end end function tglFamRecSel:action(intState) -- Action for Record Pick toggle -- V2.9 if intState == 1 then setToolTip(tglFamRecSel) -- Refresh XP Tooltip tblFamFilter[IntFilterRecord] = getRecordFilter("FAM") end end local function doRecButtonsReset(strMode) -- Reset the Delete & Change buttons and Tag Name lists -- strMode = StrModeAll if either button is used -- strMode = StrModeOld if 'Source Tag Set' is changed -- strMode = StrModeNew if 'Target Tag Set' is changed btnRecChange.Title = strRecChange btnRecChange.Active = "NO" if strMode == StrModeOld or strMode == StrModeAll then btnRecDelete.Title = strRecDelete btnRecDelete.Active = "NO" LstRecOldTag.Value = 0 strRecOldTag = nil end if strMode == StrModeNew or strMode == StrModeAll then LstRecNewTag.Value = 0 strRecNewTag = nil end end -- local function doRecButtonsReset function LstRecords:action(strText,intItem,iState) -- Action for Record Type dropdown if iState == 1 then setToolTip(LstRecords) -- Refresh XP Tooltip -- V2.4 strRecTag = TblDictionary[strText] -- Populate Source/Target Tag Set dropdown lists doRecButtonsReset(StrModeAll) tblRecFilter[IntFilterRecord] = {} -- Reset the Records filter -- V2.9 tglRecRecAll.Value = "ON" strRecOldSet,strRecNewSet = StrStrPopulateSetDropList(strRecTag,LstRecOldSet,LstRecNewSet) DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strRecOldSet,LstRecOldTag) DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strRecNewSet,LstRecNewTag) end end function btnRecDelete:action() -- Action for Delete tag button doChangeData(strRecTag,strRecOldSet,"",strRecOldTag,StrDeleteTag,tblRecFilter) doRecButtonsReset(StrModeAll) end function btnRecChange:action() -- Action for Change tag button doChangeData(strRecTag,strRecOldSet,strRecNewSet,strRecOldTag,strRecNewTag,tblRecFilter) doRecButtonsReset(StrModeAll) end function LstRecOldSet:action(strText,intItem,iState) -- Action for Source Tag Set dropdown if iState == 1 then setToolTip(LstRecOldSet) -- Refresh XP Tooltip -- V2.4 strRecOldSet = strText -- Populate Source Tag Name dropdown doRecButtonsReset(StrModeOld) DoPopulateNameDropList(strRecTag,TblOldTagIndex[strRecTag],strRecOldSet,LstRecOldTag) end end function LstRecNewSet:action(strText,intItem,iState) -- Action for Target Tag Set dropdown if iState == 1 then setToolTip(LstRecNewSet) -- Refresh XP Tooltip -- V2.4 strRecNewSet = strText -- Populate Target Tag Name dropdown doRecButtonsReset(StrModeNew) DoPopulateNameDropList(strRecTag,TblNewTagIndex[strRecTag],strRecNewSet,LstRecNewTag) end end function LstRecOldTag:action(strText,intItem,iState) -- Action for Source Tag Name dropdown if iState == 1 then setToolTip(LstRecOldTag) -- Refresh XP Tooltip -- V2.4 strRecOldTag = TblDictionary[strRecTag][strRecOldSet][strText] -- V2.5 doUpdateButtons(strRecTag,strRecOldTag,strRecNewTag,btnRecChange,strRecChange,btnRecDelete) end end function LstRecNewTag:action(strText,intItem,iState) -- Action for Target Tag Name dropdown if iState == 1 then setToolTip(LstRecNewTag) -- Refresh XP Tooltip -- V2.4 strRecNewTag = TblDictionary[strRecTag][strRecNewSet][strText] -- V2.5 doUpdateButtons(strRecTag,strRecOldTag,strRecNewTag,btnRecChange,strRecChange) end end function txtRecValue:valuechanged_cb() -- Call back when Filter Value is changed tblRecFilter[IntFilterValue] = txtRecValue.Value end function txtRecDate:valuechanged_cb() -- Call back when Filter Date is changed tblRecFilter[IntFilterDate] = txtRecDate.Value end function txtRecPlace:valuechanged_cb() -- Call back when Filter Place is changed tblRecFilter[IntFilterPlace] = txtRecPlace.Value end function tglRecRecAll:action(intState) -- Action for Record All toggle -- V2.9 if intState == 1 then setToolTip(tglRecRecAll) -- Refresh XP Tooltip tblRecFilter[IntFilterRecord] = {} end end function tglRecRecSel:action(intState) -- Action for Record Pick toggle -- V2.9 if intState == 1 then setToolTip(tglRecRecSel) -- Refresh XP Tooltip tblRecFilter[IntFilterRecord] = getRecordFilter(strRecTag) end end function btnViewLog:action() -- Action for View & Delete Log Files button local tblLogFile = {} -- Table of log file names tblLogFile[0] = 0 -- Count of log file names for _, arrAttr in ipairs ( general.GetFolderContents(StrProjectPath) ) do if arrAttr.path:match(StrProjectPath.."\\"..StrPluginTitle.." - ") then tblLogFile[0] = tblLogFile[0] + 1 -- Add log file name to table tblLogFile[tblLogFile[0]] = arrAttr.path end end GUI_LogsDialogue(tblLogFile) -- Display log file popup end function btnUpdates:action() -- Action for Check for Updates button -- V4.0 iup_gui.CheckVersionInStore(true) -- Notify if later Version end -- function btnUpdates:action function btnDefault:action() -- Action for Restore Defaults button ResetDefaultSettings() setControls() iup_gui.ShowDialogue("Main") iup_gui.DefaultDialogue() -- V4.0 SaveSettings() -- Save sticky data settings end -- function btnDefault:action function btnSetFont:action() -- Action for User Interface Font button btnSetFont.Active = "NO" iup_gui.FontDialogue(tblControls) SaveSettings() -- Save sticky data settings btnSetFont.Active = "YES" end -- function btnSetFont:action function btnSetFont:button_cb(intButton,intPress) -- Action for mouse right-click on Set Window Fonts button if intButton == iup.BUTTON3 and intPress == 0 then iup_gui.BalloonToggle() -- Toggle tooltips Balloon mode end end -- function btnSetFont:button_cb local function doExecute(strExecutable, strParameter) -- Invoke FH Shell Execute API -- V3.1 local function ReportError(strMessage) iup_gui.WarnDialogue( "Shell Execute Error", "ERROR: "..strMessage.." :\n"..strExecutable.."\n"..strParameter.."\n\n", "OK" ) end -- local function ReportError return general.DoExecute(strExecutable, strParameter, ReportError) end -- local function doExecute local strHelp = "https://pluginstore.family-historian.co.uk/page/help/change-any-fact-tag" local arrHelp = { "-individual-records-tab"; "-family-records-tab"; "-other-records-tab"; "-status-and-settings-tab"; } function btnGetHelp:action() -- Action for Help and Advice button -- V3.1 local strPage = arrHelp[intTabPosn] or "" doExecute( strHelp..strPage ) fhSleep(3000,500) dialogMain.BringFront="YES" end -- function btnGetHelp:action iup_gui.ShowDialogue("Main",dialogMain,btnClose,"map") -- Map the Main dialogue for dropdown lists, etc iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes strRecTag = StrPopulateDropdownLists("INDI") -- Populate the Individual tab dropdown lists -- 17 Aug 2013 if #ArrLogFileText > 0 then LblLogFile.Title = StrLogFilePath end -- Update the log filename display -- V3.7 progbar.Setup(iup_gui.DialogueAttributes("Bars")) -- Setup the progress bar attributes iup_gui.ShowDialogue("Main") -- Show the Main dialogue if flgQuitMode then SaveSettings() -- Save sticky data settings if #TblPtrName > 0 then -- If no changes then just Close else message to use Undo Plugin Updates -- V3.7 fhMessageBox("\n To reverse all changes please use the command \n\n Edit > Undo Plugin Updates \n","MB_OK","MB_ICONINFORMATION") end end end -- function GUI_MainDialogue -- Main body of Plugin script starts here -- local intPause = collectgarbage("setpause",50) -- Default = 200 Aggressive = 50 -- Sets pause of collector and returns prev value of pause -- V3.4 local intStep = collectgarbage("setstepmul",300) -- Default = 200 Aggressive = 300 -- Sets step mult of collector & returns prev value of step -- V3.4 fhInitialise(5,0,8,"save_recommended") -- V5.0.2 fhCreateItem FILE/FORM update, V5.0.8 for sticky settings with scope PresetGlobalData() -- Preset global data definitions ResetDefaultSettings() -- Preset default sticky settings LoadSettings() -- Load sticky data settings LoadFactsAndTags() -- Load all defined Fact Sets, undefined Custom Facts, Gedcom Defined Tags, and UDF Tag Sets GUI_MainDialogue() -- Invoke main dialogue SaveSettings() -- Save sticky data settings if #TblPtrName > 0 then -- Create Query Result Set fhOutputResultSetTitles("Results Log") fhOutputResultSetColumn("Record Name", "item", TblPtrName, #TblPtrName, 120, "align_left") fhOutputResultSetColumn("Old Item" , "text", TblOldItem, #TblPtrName, 060, "align_left") fhOutputResultSetColumn("Old Data" , "text", TblOldData, #TblPtrName, 200, "align_left") fhOutputResultSetColumn("Action" , "text", TblPerform, #TblPtrName, 030, "align_left") fhOutputResultSetColumn("New Item" , "text", TblNewItem, #TblPtrName, 060, "align_left") fhOutputResultSetColumn("Link Data", "item", TblPtrData, #TblPtrName, 200, "align_left") end

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