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
--[[
@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