Search and Replace.fh_lua--[[
@Title: Search and Replace
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 4.1
@Keywords:
@LastUpdated: 05 Mar 2025
@Licence: This plugin is copyright (c) 2025 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: Searches for and replaces any given text or date within the current project, with an option to ask for confirmation for each change.
At the end, lists all changed fields with links back to the record (double-click on an item to view it in the Property Box).
@TBD: Check replacement success/failure for duplicate Place names, etc; New option to support external files;
@V4.1: Make StrFileWarning() conditional on Confirm option; Add Source Template ~FDEF.CODE & ~FDEF.TYPE tags;
@V4.0: Revised how Multimedia Linked File fields and Media files filter operates; See StrFileWarning() and doWarning() and doMediaWarning();
@V3.9: Allow Unicode filepaths when changing File names;
@V3.8: Correct 'Case Sensitive' mispelling; Fix broken templated source metafields; Add extra templated source Place & Address options; Add Place and Address field options;
@V3.7: Fix problem with < > in plain text fields;
@V3.6: Fix odd custom fact names like 'An (ODD) Fact (INDI.EVEN-AN_ODD_FACT)'; Update to Library V3.3;
@V3.5: Add Standardized _PLAC.STAN to Record Names/Titles; Updated some other field names;
@V3.4: Add collectgarbage() memory management; Fix for Data Class of Metafields;
@V3.3: Correct updating of Rich Text involving object links;
@V3.2: Replace Place records first to avoid new records in DoAllItems(); Preserve rich text record links in doManageString(...); Minor updates to Major Options tab dialogue;
@V3.1: Updated library to Functions Prototypes v3.0; iup_gui.Balloon = "NO" for PlayOnLinux/Mac; Show/Hide white space in Result Set; Clear Result Set if no data; Prompt GUI taller wordwrap boxes; FH V7 Lua 3.5 IUP 3.28;
@V3.0: Libray V2.9, Show/Hide white space chars as in Word, rearrange Search Scope, Basic Filters, Search Criteria on Major Options tab, add LMO/Sort Date (~OBJE._DATE).
@V2.9: When Media Linked File is Replaced then similarly alter Media file path itself, include FILE tag previously omitted, and cater for both OBJE.FORM and PLAC.FORM tags.
@V2.8: Fix StrSP where [ needs no % escape, new Select Records Search Scope filter, and updated library modules.
@V2.7: Extra format checks and completion report for Import Preset Files, and other minor improvements.
@V2.6: Initialisation progress bars for large Projects, add multi-selection and Import/Export Presets to Manage Presets dialogue.
@V2.5: Support named local Project Presets and global Program Presets, revised tblFilters titles & tooltips, and change for strDataClass == "date" in DoSeekMatch().
@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 uses NaturalSize,
@V2.4: iup_gui update, fix test for newline in Search/Replace, add Place records & Witness Role fields, update ToolTips for ShortText, replace CP1252 … with ASCII ~ to allow both ANSI & UTF-8 encoding.
@V2.3: Date Phrase Warnings inactive when Date unticked, add BalloonToggle().
@V2.2: iup_gui update to fix positioning of Confirmation popup after Restore Defaults, fix for interpreted Date Phrases, toggle for Date Phrase Warnings, and other minor fixes.
@V2.1: iup_gui update, comments removed, Attribute values & other dropdown lists, support tab & newline, inhibit Whole Words for spaces/punctuation, allow repeated words, fix XP tooltips, etc.
@V2.0: Better Item Details, Attribute values, more field search options, Search Only mode, updated GUI, Help & Advice, Sticky Settings, Libraries, etc.
@V1.7: Add the Date and specific Tag related field search & replace capability (see lines marked with -- V1.7) plus major code tidy.
@V1.6: Add more details to the replace prompt for the context of the field being changed.
@V1.5: General code tidy (not published).
@V1.4: Add options for whole word search only and prevent wild card patterns being used automatically
@V1.3: Add Case insensitive searching and cancel or close the window will abort the replaces, while still listing the changes made.
@V1.2: Change to Provide a Skip Button, to cancel click the close button on the window.
@V1.1: Add Skip tick box to replace prompt.
--]]
if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end
--[[
@Title: aa Library Functions Preamble
@Author: Mike Tate
@Version: 3.7
@LastUpdated: 12 Dec 2024
@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.0
@LastUpdated: 27 Aug 2020
@Description: Progress Bar library module.
@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.CENTER -- Show window default position is central
local intPosY = iup.CENTER
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
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.2
@LastUpdated: 07 Oct 2024
@Description: Graphical User Interface Library Module
@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, Help, 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 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","Help","Main" by default
local arg = {...}
local tblNames = {"Font";"Help";"Main";}
for intName, strName in ipairs(arg) do
if type(strName) == "string"
and strName ~= "Font"
and strName ~= "Help"
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.CENTER
tblName.CoordY = iup.CENTER
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(0,intX,intMaxW-intWide) -- Ensure X co-ordinate positions window on screen
end
if intY and intY < iup.CENTERPARENT then
intY = intDimension(0,intY,intMaxH-intHigh) -- Ensure Y co-ordinate positions window on screen
end
tblName.Raster = strRas or "x"
tblName.CoordX = tonumber(intX) or iup.CENTER
tblName.CoordY = tonumber(intY) or iup.CENTER
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","Help","Main" by default
for intName, strName in ipairs(tblOfNames(...)) do
local tblName = tblNameFor(strName)
--# tblName.Raster = tostring(fh.LoadLocal(strName.."S",tblName.Raster)) -- Legacy of "S" becomes "R"
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.FontSet = tonumber(fh.LoadGlobal("Fonts" ,fh.FontSet)) -- Legacy only
fh.FontSet = tonumber(fh.LoadGlobal("FontSet" ,fh.FontSet)) -- Legacy only
fh.History = tostring(fh.LoadGlobal("History" ,fh.History))
fh.Balloon = tostring(fh.LoadGlobal("Balloon" ,fh.Balloon, "Machine"))
fh.LoadDialogue(...)
if fh.FontSet > 0 then fh.FontAssignment(fh.FontSet) end -- Legacy only
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
--# doSaveData(fh.ComputerName.."-"..strParam,strFolder,strScope) -- Uses relative paths to let Paths change
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","Help","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
if tonumber(tblName.CoordX) == nil
or tonumber(tblName.CoordX) < 0 -- V3.5
or tonumber(tblName.CoordX) > intMaxW then
tblName.CoordX = iup.CENTER
end
if tonumber(tblName.CoordY) == nil
or tonumber(tblName.CoordY) < 0 -- V3.5
or tonumber(tblName.CoordY) > intMaxH then
tblName.CoordY = iup.CENTER
end
end -- function CheckWindowPosition
function fh.IsNormalWindow(iupDialog) -- Check dialogue window is not Maximised or Minimised (now redundant)
-- tblPosn[1] = origin x, tblPosn[2] = origin y, tblPosn[3] = width, tblPosn[4] = height
local tblPosn = stringx.splitnumbers(iupDialog.ScreenPosition)
local intPosX = tblPosn[1]
local intPosY = tblPosn[2]
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
return true
end -- function IsNormalWindow
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]
local intPosY = tblPosn[2]
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
iupDialog.MaxSize = intMaxW.."x"..intMaxH -- Maximum size is screen size
iupDialog.MinSize = "x" -- Minimum size (default "x" becomes nil)
iupDialog.RasterSize = tblName.Raster or "x" -- Raster size (default "x" becomes nil)
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) fh.SetWindowCoord(tblName) end -- V3.5
iupDialog.resize_cb = iupDialog.resize_cb or function(self) if fh.SetWindowCoord(tblName) then tblName.Raster=self.RasterSize end end -- V3.5
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
fh.RefreshDialogue(strName) -- Refresh to set Natural Size as Minimum Size
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.CENTER
tblName.CoordY = nil -- iup.CENTER
else
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 ")
---[=[
local intFontPlain = 1 -- Font Face & Style values for legacy FontSet setting
local intFontBold = 2
local intArialPlain = 3
local intArialBold = 4
local intTahomaPlain= 5
local intTahomaBold = 6
local strFontFace = fh.FontBody:gsub(",.*","")
local tblFontSet = {} -- Lookup table for FontHead and FontBody
tblFontSet[intFontPlain] = { Head=strFontFace.."; Bold -16"; Body=strFontFace.."; -15"; }
tblFontSet[intFontBold] = { Head=strFontFace.."; Bold -16"; Body=strFontFace.."; Bold -15"; }
tblFontSet[intArialPlain] = { Head="Arial; Bold -16"; Body="Arial; -16"; }
tblFontSet[intArialBold] = { Head="Arial; Bold -16"; Body="Arial; Bold -15"; }
tblFontSet[intTahomaPlain] = { Head="Tahoma; Bold -15"; Body="Tahoma; -16"; }
tblFontSet[intTahomaBold] = { Head="Tahoma; Bold -15"; Body="Tahoma; Bold -14"; }
function fh.FontAssignment(intFontSet) -- Assign Font Face & Style GUI values for legacy FontSet setting
if intFontSet then
intFontSet = math.max(intFontSet,1)
intFontSet = math.min(intFontSet,#tblFontSet)
fh.FontHead = tblFontSet[intFontSet]["Head"] -- Legacy Font for all GUI dialog header text
fh.FontBody = tblFontSet[intFontSet]["Body"] -- Legacy Font for all GUI dialog body text
end
end -- function FontAssignment
--]=]
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 and Help dialogues
local tblHelp = tblNameFor("Help")
if type(tblHelp.Dialog) == "userdata" then -- Help dialogue exists
fh.AssignAttributes(tblHelp.TblAttr) -- Assign the Help dialogue attributes
fh.RefreshDialogue("Help") -- Refresh the Help window size & position
end
fh.AssignAttributes(tblAttr) -- Assign parent dialogue attributes
fh.RefreshDialogue(strName) -- Refresh parent window size & position and bring infront of Help 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 end -- Return label control so message can be changed
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 -- V3.9
local strVersion = "0"
if strPlugin then
local strFile = fh.MachinePath.."\\VersionInStore "..strPlugin..".dat"
local intTime = os.time() - 2600000 -- Time in seconds a month ago -- V3.9
local tblAttr, strError = lfs.attributes(strFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago -- V3.9
local strErrFile = fh.MachinePath.."\\VersionInStoreInternetError.dat"
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..strPlugin
local isOK, strReturn = pcall(httpRequest,strRequest)
if not isOK then -- Problem with Internet access
local intTime = os.time() - 36000 -- Time in seconds 10 hours ago
local tblAttr, strError = lfs.attributes(strErrFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ","MB_OK","MB_ICONEXCLAMATION")
end
general.SaveStringToFile(strErrFile,strErrFile) -- Update file modified time
else
general.DeleteFile(strErrFile) -- Delete file if Internet is OK
if strReturn ~= nil then
strVersion = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
general.SaveStringToFile(strVersion,strFile) -- Update file modified time and save version -- V4.1
end
end
else
strVersion = general.StrLoadFromFile(strFile) -- Retrieve saved latest version -- V4.1
if #strVersion > 9 then general.DeleteFile(strFile) end
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
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 Family Historian '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.CustomDialogue("Help","1020x730") -- Custom "Help" dialogue sizes
fh.DefaultDialogue() -- Default "Font","Help","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()
iup_gui.Gap = "2"
iup_gui.Balloon = "NO" -- Needed for PlayOnLinux/Mac -- V3.1
iup_gui.SetUtf8Mode()
StrSP = "\t-\r !-/:-@[-`{-~" -- Substitute LUA pattern for %s%p ( [ is not magic here so needs no % escape )
StrMid_Dot = "·" -- Space symbol
StrRtArrow = "»" -- Hor Tab symbol
StrPilcrow = "¶" -- Newline symbol
if fhGetAppVersion() > 5 then
StrMid_Dot = fhConvertANSItoUTF8(StrMid_Dot) -- White space UTF8 symbols -- V3.0
StrRtArrow = fhConvertANSItoUTF8(StrRtArrow)
StrPilcrow = fhConvertANSItoUTF8(StrPilcrow)
end
DicEncode = { } -- White space encoding -- V3.0
DicEncode[" "] = StrMid_Dot
DicEncode["\t"] = StrRtArrow.."\t"
DicEncode["\n"] = StrPilcrow.."\n"
IntCaret = -9 -- Text edit caret position -- V3.0
ArrOption = {} -- Array of global Program Preset GUI options -- V2.5
TblOption = {} -- Table of local Project Preset GUI options
TblRecord = {} -- Result Set tables
TblDataRef = {}
TblOldValue = {}
TblNewValue = {}
TblRefValue = {} -- V3.1
TblResultSet= { Title="Search and Replace "; Sub="No Matching Data"; Old="Data Value"; }
end -- function PresetGlobalData
-- Set User Default Options --
function UserDefaultOptions(tblOption)
if not tblOption.Val then tblOption.Val = {} end -- Search and Replace strings are in Val sub-table
if not tblOption.Lst then tblOption.Lst = {} end -- All dropdown list settings are in Lst sub-table
if not tblOption.Tgl then tblOption.Tgl = {} end -- All toggle ON/OFF settings are in Tgl sub-table
tblOption.Val.Seek = "" -- Search text string empty
tblOption.Val.Swap = "" -- Replace text string empty
for strName, anyValue in pairs (TblOption.Lst) do
tblOption.Lst[strName] = "" -- Reset all GUI dropdown lists associated with toggles
end
for strName, anyValue in pairs (TblOption.Tgl) do
tblOption.Tgl[strName] = "ON" -- Enable all GUI toggle options except as below
end
tblOption.Tgl.Conf = "ON" -- V2.1 Ensure Confirmation toggle is ticked
tblOption.Tgl["SOUR.TITL"]= "ON" -- "OFF" -- V2.1 Parent Child Tag for Source Titles -- V1.7
tblOption.Tgl.age = "ON" -- "OFF" -- V2.1 Class age is just the Age fields -- V1.7
tblOption.Tgl.Age = "OFF" -- V2.1 Age field error checks
tblOption.Tgl.date = "OFF" -- V1.7 Class date is all the Date fields
tblOption.Tgl.Date = "OFF" -- V1.7 Date field warnings
tblOption.Tgl.Phrase = "ON" -- V2.2 Date Phrase warnings
end -- function UserDefaultOptions
-- 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("Preset","0x0") -- Custom "Preset" dialogue minimum size & centralisation
iup_gui.DefaultDialogue("Preset","Bars","Memo") -- GUI window rastersize and X & Y co-ordinates for "Main","Font","Preset","Bars","Memo" dialogues
IntTabPosn = 0 -- Tab position undefined
UserDefaultOptions(TblOption) -- GUI Modes are mixed case, Classes are lower case, Tags are upper case
if not TblOption.Set then TblOption.Set = {} end -- Non-toggle option settings are in Set sub-table
TblOption.Set.Item = 0 -- Item count of database size
TblOption.Set.Date = "" -- Date of last Item update
TblOption.Set.Recs = nil -- Record Ids Search Scope filter off -- V2.8
local intCurr = tonumber(TblOption.Set.Curr) or 1
TblOption[intCurr] = {} -- Clear current Project Preset settings -- V2.5
if TblOption.Set.List then
TblOption.Set.List[intCurr].Lock = "OFF"
end
IsMemoFail = true -- V3.4
end -- function ResetDefaultSettings
-- Load Sticky Settings from File --
function LoadSettings()
iup_gui.LoadSettings("Preset","Bars") -- Includes "Main","Font" dialogues and "FontSet" & "History"
ArrOption = iup_gui.LoadGlobal("Option",ArrOption,"Machine") -- V2.5
TblOption = iup_gui.LoadGlobal("Option",TblOption)
if not ArrOption.Set then ArrOption.Set = {} end
if not ArrOption.Set.List then ArrOption.Set.List = {} end -- V2.5
for intList = 1, #ArrOption.Set.List do
if not ArrOption[intList] then ArrOption[intList] = {} end -- V2.5 Ensure each Program Preset entry exists
end
if not TblOption.Set then TblOption.Set = {} end -- Cope with legacy settings
if not TblOption.Val then TblOption.Val = {} end
if not TblOption.Lst then TblOption.Lst = {} end
if TblOption.Set.Lock then TblOption.Set.Lock = nil end -- V2.5
local arrList = {}
if not TblOption.Set.List then
TblOption.Set.List = {}
TblOption.Set.Curr = 1
arrList = { "Alpha"; "Bravo"; "Charlie"; "Delta"; } -- Table of local Project Preset names -- V2.5
elseif type(TblOption.Set.List[1]) == "string" then
arrList = TblOption.Set.List -- Legacy local Project Preset list
end
for intList, strName in ipairs (arrList) do
TblOption.Set.List[intList] = { Name=strName; Lock="OFF"; }
end
for intList = 1, #TblOption.Set.List do
if not TblOption[intList] then TblOption[intList] = {} end -- V2.5 Ensure each Project Preset entry exists
if not TblOption.Set.List[intList].Lock then TblOption.Set.List[intList].Lock = "OFF" end
end
TblOption.Set.Curr = math.min(tonumber(TblOption.Set.Curr) or 1,#TblOption.Set.List)
TblOption.Val.Seek = TblOption.Set.Seek -- V2.5 Move legacy Search & Replace strings
TblOption.Val.Swap = TblOption.Set.Swap
TblOption.Set.Seek = nil
TblOption.Set.Swap = nil
TblOption.Tgl["SOUR.TITL"]= TblOption.Tgl["SOUR.TITL"] or "ON" -- V1.7 Parent Child Tag for Source Titles
TblOption.Tgl.Conf = TblOption.Tgl.Conf or "ON" -- V2.1 Ensure Confirmation toggle is ticked
TblOption.Tgl.age = TblOption.Tgl.age or "ON" -- V1.7 Class age is just the Age fields
TblOption.Tgl.Age = TblOption.Tgl.Age or "OFF" -- V2.1 Age field error checks
TblOption.Tgl.date = TblOption.Tgl.date or "OFF" -- V1.7 Class date is all the Date fields
TblOption.Tgl.Date = TblOption.Tgl.Date or "OFF" -- V1.7 Date field warnings
TblOption.Tgl.Phrase = TblOption.Tgl.Phrase or "ON" -- V2.2 Date Phrase warnings
SaveSettings() -- Save sticky data settings
end -- function LoadSettings
-- Save Sticky Settings to File --
function SaveSettings()
local datToday = fhNewDate(2000)
datToday:SetSimpleDate(fhCallBuiltInFunction("Today")) -- Obtain date today
local strToday = datToday:GetDisplayText("ABBREV")
if TblOption.Set.Date ~= strToday or not TblOption.Set.INDI then -- Different date or no record type counts -- V2.3
TblOption.Set.Date = strToday -- So count database items for progress bar -- V2.3
progbar.Setup()
progbar.Start("Initialisation",fhGetRecordTypeCount()) -- Initialisation Progress Bar -- V2.6
local intItem = 0
for strType in iterate.RecordTypes() do -- Loop through all record types -- V2.3
progbar.Message("Counting "..strType.." Record Items ~ Please Wait")
progbar.Step() -- Step progress bar
local intType = 0
for ptrItem in iterate.Items(strType) do -- Loop through all record type items
intType = intType + 1 -- Count number of record type items -- V2.3
intItem = intItem + 1 -- Count number of all database items
if ( intItem % 8384 ) == 0 and progbar.Stop() then error("\n Plugin Aborted. ") end
end
TblOption.Set[strType] = intType -- Save count of record type items -- V2.3
end
TblOption.Set.Item = intItem -- Save count of all database items -- V2.3
progbar.Close()
end
iup_gui.SaveGlobal("Option",TblOption)
iup_gui.SaveGlobal("Option",ArrOption,"Machine") -- V2.5
iup_gui.SaveSettings("Preset","Bars") -- Includes "Main","Font" dialogues and "FontSet" & "History"
end -- function SaveSettings
function strHideWhiteSpace(strText,strChar) -- Remove special white space symbols, or replace with \01 byte -- V3.0
strChar = strChar or ""
strText = strText:gsub(StrMid_Dot," "):gsub(StrRtArrow,strChar):gsub(StrPilcrow,strChar)
return strText
end -- function strHideWhiteSpace
function strShowWhiteSpace(strText) -- Insert special white space symbols, after removing \01 byte -- V3.0
strText = strText:gsub("\01","")
strText = strText:gsub("([ \t\n])",DicEncode)
return strText
end -- function strShowWhiteSpace
function doChange(iupSelf) -- Handle text edits when showing white space symbols -- V3.0
local intPosn = tonumber(iupSelf.CaretPos)
if not TblOption.Tgl.WhSp
or TblOption.Tgl.WhSp == "OFF" then
local strText = strHideWhiteSpace(iupSelf.Value,"\01") -- Replace » and ¶ multi-byte symbols with \01 byte
local strPrev = strText:sub(intPosn-1,intPosn-1)
local strCurr = strText:sub(intPosn,intPosn)
local strNext = strText:sub(intPosn+1,intPosn+1)
if strCurr == "\01" or strNext == "\t" or strNext == "\n" then -- Deletion of Tab or Newline with Backspace or Delete key
if strCurr ~= "\01" then intPosn = intPosn + 1 end -- Adjust caret position when Delete key deletes » or ¶ symbol
strText = strText:sub(1,intPosn-1)..strText:sub(intPosn+1) -- Remove either \01 byte or Tab/Newline character
intPosn = intPosn - 1 -- Adjust caret position for deleted character
end
if strPrev ~= "\01" and (strCurr == "\t" or strCurr == "\n") then -- Insertion of Tab or Newline
intPosn = intPosn + 1 -- Adjust caret position for inserted » or ¶ symbol
end
iupSelf.Value = strShowWhiteSpace(strText)
iupSelf.CaretPos = intPosn
end
IntCaret = intPosn
end -- function doChange
function doCaret(iupSelf,intPosn) -- Handle cursor moves when showing white space symbols -- V3.0
if not TblOption.Tgl.WhSp
or TblOption.Tgl.WhSp == "OFF" then
local strText = strHideWhiteSpace(iupSelf.Value,"\01") -- Replace » and ¶ multi-byte symbols with \01 byte
if strText:sub(intPosn,intPosn) == "\01" then -- Skip over special » and ¶ symbols
if intPosn > IntCaret then
intPosn = intPosn + 1 -- Moving left to right
else
intPosn = intPosn - 1 -- Moving right to left
end
iupSelf.CaretPos = intPosn
end
end
IntCaret = intPosn
end -- function doCaret
function StrAgeWarning(ptrItem,strNewAge) -- Returns warning text or nil if OK, plus the new Age text
-- ptrItem ~ Pointer to age item
-- strNewAge ~ New value for age item
if #strNewAge > 0 and TblOption.Set.Mode == "Replace" then -- No age error checks unless Age Item exists in Replace mode
local strOldAge = fhGetValueAsText(ptrItem)
if TblOption.Tgl.Age then -- Age field warnings enabled?
fhSetValueAsText(ptrItem,strNewAge) -- Check for invalid Age returning blank
local strAgeText = fhGetValueAsText(ptrItem)
fhSetValueAsText(ptrItem,strOldAge) -- Restore the original Age value
if strAgeText == "" then
return " ! ! BEWARE ! ! Replacement age is invalid. \n The field will be DELETED if 'Replace' is confirmed. ",strNewAge
else
return nil,strAgeText -- Otherwise, return nil warning with corrected Age text
end
end
end
return nil,strNewAge -- For OK values or no warning return nil, and original text
end -- function StrAgeWarning
function StrDateWarning(ptrItem,strNewVal) -- Returns warning text or nil if date is OK
-- ptrItem ~ Pointer to date item
-- strNewVal ~ New value for date item
if #strNewVal > 0 and TblOption.Set.Mode == "Replace" then -- No date warnings unless Date Item exists in Replace mode
local oldDate = fhGetValueAsDate(ptrItem)
local newDate = fhNewDate(1999) -- For "date" value type use Date Object
local strIDP = " %(\".*\"%)$" -- LUA pattern for Interpreted 'Date ("Phrase")' -- V2.2
if newDate:SetValueAsText(strNewVal:gsub(strIDP,""),false) -- Is new Date valid, allowing for Interpreted Date Phrase -- V2.2
and newDate:SetValueAsText(strNewVal,true) then
if TblOption.Tgl.Date then -- Date field warnings enabled?
fhSetValueAsDate(ptrItem,newDate) -- Check for invalid Date warnings
local strWarning = fhCallBuiltInFunction("GetDataWarning",ptrItem,1)
fhSetValueAsDate(ptrItem,oldDate) -- Restore the original Date value
if strWarning ~= "" then
return " ! ! BEWARE ! ! Replacement date is inconsistent. \n "..strWarning.." \n It will be set anyway if 'Replace' is confirmed. "
end
end
elseif TblOption.Tgl.Phrase then -- Date Phrase warnings enabled? -- V2.2
if strNewVal:match(strIDP) then -- Cater for Interpreted Date Phrase and plain Date Phrase -- V2.2
return " ! ! BEWARE ! ! Replacement interpreted date is invalid. \n It will become part of the Date Phrase if 'Replace' is confirmed. "
else
return " ! ! BEWARE ! ! Replacement date is invalid. \n It will be set as a Date Phrase if 'Replace' is confirmed. "
end
end
end
return nil -- For OK values or no warning return nil
end -- function StrDateWarning
function StrFileWarning(ptrItem,strNewVal,strOldVal) -- Returns warning text for Media file issues or nil otherwise -- V4.0
-- ptrItem ~ Pointer to data item
-- strNewVal ~ New value for data item
-- strOldVal ~ Old value for data item
local strWarn = nil
if fhGetTag(ptrItem):match("FILE") and TblOption.Set.Mode == "Replace" then -- Updating linked Media file path -- V4.0
local strPath = general.SplitFilename(fhGetContextInfo("CI_GEDCOM_FILE"))
if not general.FlgFileExists(StrMediaRelToAbs(strPath,strNewVal)) then
if general.FlgFileExists(StrMediaRelToAbs(strPath,strOldVal)) then
strWarn = " ! ! BEWARE ! ! See the Help && Advice for details. \n\n"
local _,_,strOldExt = general.SplitFilename(strOldVal) -- Replacement Value file does not exist and Current Value file does exist
local _,_,strNewExt = general.SplitFilename(strNewVal)
strOldExt = strOldExt:upper()
strNewExt = strNewExt:upper()
if strOldExt == strNewExt then
if TblOption.Tgl.Conf then -- Warning conditional on confirmation setting -- V4.1
strWarn = strWarn .. " The Current Value file will be copied to the Replacement Value file if 'Replace' is confirmed. "
else
strWarn = nil
end
else
strWarn = strWarn .. " This change is not allowed as it involves altering the file format from "..strOldExt.." to "..strNewExt..". "
end
end
end
end
return strWarn
end -- function StrFileWarning
function GetDataClass(ptrItem,strChild) -- Get data class & child tag for all items including metafields -- V3.4 -- V3.8
-- ptrItem ~ Pointer to data item
-- strChild ~ Current child tag
local tblMeta = {
TX = { Child="TEXT" ; Class="text" ; } ;
NM = { Child="NAME" ; Class="name" ; } ;
DT = { Child="DATE" ; Class="date" ; } ;
PL = { Child="PLAC" ; Class="place" ; } ;
AD = { Child="ADDR" ; Class="longtext" ; } ;
RP = { Child="LINK" ; Class="link" ; } ;
EN = { Child="ENUM" ; Class="enumword" ; } ;
UL = { Child="URL" ; Class="url" ; } ;
}
local strClass = fhGetDataClass(ptrItem) -- Get basic item data class
if strClass == "metafield" then
local dicMeta = tblMeta[ fhGetMetafieldType(ptrItem) ]
strChild = "~_FIELD."..dicMeta.Child -- Add metafield child tag
strClass = dicMeta.Class -- Get metafield data class
end
return strClass,strChild
end -- function GetDataClass
-- Search & Replace Prompt GUI --
function SearchReplacePrompt(ptrItem,strOldVal,strNewVal,strWarning)
-- ptrItem ~ Pointer to data item
-- strOldVal ~ Old value of data item
-- strNewVal ~ New value for data item
-- strWarning~ Warning message for Age/Date value
local function intLineCount(strText,maxLines)
--[[
@Function: intLineCount
@Description:Works out approximate number of lines to display a text string in a GUI text box.
@Parameters:
strText ~ String to process mandatory
maxLines ~ Maximum height for text box, defaults to 9 lines if not provided
]]
maxLines = maxLines or 9
local intLines = 1
for strLine in string.gmatch(strText.."\n","(.-)\n") do -- Count lines and increase for long wrapped lines -- V3.1
intLines = intLines + math.ceil( #strLine / 150 )
end
return math.min(intLines,maxLines)
end -- local function intLineCount
local function strItemDetails(ptrItem) -- Returns Record Tag and [Id] plus full Data Ref, and descriptive text for each Tag level
-- ptrItem ~ Pointer to data item
local tblDetails = {}
local ptrTemp = ptrItem:Clone()
local strDataRef, intRecId, strRecTag = general.BuildDataRef(ptrItem)
TblOption.Set.Data = strDataRef -- Save the Data Ref for Result Set
table.insert(tblDetails,strRecTag.." Record Id: ["..intRecId.."] Data Ref: "..strDataRef)
while ptrTemp:IsNotNull() do
local strUDF = ""
if fhIsUDF(ptrTemp) then -- V2.0 adds UDF indication
strWarning = " ! ! BEWARE ! ! Unrecognised Data Field (UDF)"
strUDF = " {*UDF!}"
end
table.insert(tblDetails,2,fhGetDisplayText(ptrTemp)..strUDF)
ptrTemp:MoveToParentItem(ptrTemp)
end
return table.concat(tblDetails,"\n")
end -- local function strItemDetails
local function iupMultiLineText(strVal,strRead) -- Return iup.text multiline control
-- strVal ~ Text value
-- strRead ~ "Yes" or "No"
return iup.text { Value=strVal, ReadOnly=strRead, VisibleLines=intLineCount(strVal,6), -- Add WordWrap & taller boxes for long text -- V3.1
MultiLine="Yes"; WordWrap="Yes"; AutoHide="Yes"; Formatting="Yes"; AddFormatTag=iup.user { Bulk="Yes"; CleanOut="Yes"; }; }
end -- local function iupMultiLineText
local strButton = "Cancel" -- Default to value for X Close window button
local function iupButtonControl(strTitle) -- Return iup.button control
return iup.button { Title=strTitle; MinSize="100x40"; action=function(self) strButton=self.Title return iup.CLOSE end; }
end -- local function iupButtonControl
if not TblOption.Tgl.WhSp then -- V3.0
strOldVal = strShowWhiteSpace(strOldVal)
strNewVal = strShowWhiteSpace(strNewVal)
end
local strMode = TblOption.Set.Mode -- Mode is either "Replace" or "Report" text
local lblDataRef = iup.label { Title="Data Reference : "; }
local txtDataRef = iupMultiLineText( strItemDetails(ptrItem), "Yes" )
local lblCurrent = iup.label { Title="Current Value : "; }
local txtCurrent = iupMultiLineText( strOldVal, "Yes" )
local lblReplace = iup.label { Title="Replacement Value : "; }
local txtReplace = iupMultiLineText( strNewVal, "No" )
local lblWarning = iup.label { Title=strWarning; }
local tglConfirm = iup.toggle { Title="Confirm every item found"; }
local btnPerform = iupButtonControl( strMode )
local btnSkipOne = iupButtonControl( "Skip" )
local btnDestroy = iupButtonControl( "Cancel" )
local function doWarning() -- Update Warning label and Replace button -- V4.0
btnPerform.Active = "Yes"
lblWarning.Title = strWarning
if strWarning then
lblWarning.Active = "Yes"
if strWarning:match(" not allowed ") then -- Disable the Replace button -- V4.0
btnPerform.Active = "No"
end
else
lblWarning.Active = "No"
end
end -- local function doWarning
doWarning() -- V4.0
if strMode ~= "Replace" then -- Hide the Replacement Value controls
lblReplace.Visible = "No"
lblReplace.Floating = "Yes"
txtReplace.Visible = "No"
txtReplace.Floating = "Yes"
end
if TblOption.Tgl.Conf then -- Set initial Confirmation toggle value
tglConfirm.Value = "ON"
else
tglConfirm.Value = "OFF"
end
local tblConfirm = { ON = "Confirmation prompt for each item found"; OFF = strMode.." all matching text unconditionally"; }
-- Set other GUI control attributes
for iupName, tblAttr in pairs ( {
-- Control = 1~FgColor ; 2~Font ; 3~Expand ; 4~Tip ; 5~action ;
[lblDataRef] = { iup_gui.Head; iup_gui.FontHead ; "No" ; "Data reference details" ; };
[txtDataRef] = { iup_gui.Body; iup_gui.FontBody ; "Horizontal" ; "Data reference details" ; };
[lblCurrent] = { iup_gui.Head; iup_gui.FontHead ; "No" ; "Current text string" ; };
[txtCurrent] = { iup_gui.Body; iup_gui.FontBody ; "Horizontal" ; "Current text string" ; };
[lblReplace] = { iup_gui.Head; iup_gui.FontHead ; "No" ; "Replacement text string that can be edited" ; };
[txtReplace] = { iup_gui.Safe; iup_gui.FontBody ; "Horizontal" ; "Replacement text string that can be edited" ; };
[lblWarning] = { iup_gui.Risk; iup_gui.FontBody ; "Yes" ; "Data warning message" ; };
[tglConfirm] = { iup_gui.Safe; iup_gui.FontBody ; "Yes" ; tblConfirm[tglConfirm.Value]; function() tglConfirm.Tip = tblConfirm[tglConfirm.Value] end; };
[btnPerform] = { iup_gui.Safe; iup_gui.FontBody ; "Horizontal" ; strMode.." the data item" ; };
[btnSkipOne] = { iup_gui.Safe; iup_gui.FontBody ; "Horizontal" ; "Skip this data item" ; };
[btnDestroy] = { iup_gui.Risk; iup_gui.FontBody ; "Horizontal" ; "Cancel rest of search" ; };
} ) do
iupName.FgColor = tblAttr[1]
iupName.Font = tblAttr[2]
iupName.Expand = tblAttr[3]
iupName.Tip = tblAttr[4]
iupName.TipBalloon = iup_gui.Balloon
if tblAttr[5] then iupName.action = tblAttr[5] end
end
local boxDataRef = iup.vbox { lblDataRef; txtDataRef; }
local boxCurrent = iup.vbox { lblCurrent; txtCurrent; }
local boxReplace = iup.vbox { lblReplace; txtReplace; }
local boxControls = iup.vbox { lblWarning; iup.hbox { tglConfirm; btnPerform; btnSkipOne; btnDestroy; Margin="3x3"; Alignment="ABOTTOM"; }; }
local boxDialogue = iup.vbox { boxDataRef; boxCurrent; boxReplace; boxControls; }
local dlgPerform = iup.dialog { Title=iup_gui.Plugin..iup_gui.Version; Gap=iup_gui.Gap; Margin="3x3"; boxDialogue; }
local strDataClass = GetDataClass(ptrItem) -- Fix for metafields -- V3.4
function txtReplace:action(intChar,strText) -- Replacement Value is being changed
-- intChar ~ Character position
-- strText ~ Text string (not used)
if strDataClass ~= "longtext" and string.char(intChar) == "\n" then
return iup.IGNORE -- Prevent short text items exceeding one line
end
end -- function txtReplace:action
function txtReplace:valuechanged_cb() -- Replacement Value has been changed
if strDataClass == "age" then -- Let spaces be added to allow yrs/mns/dys to be entered
local strNewVal = strHideWhiteSpace(strNewVal) -- V3.0
local strTxtVal = strHideWhiteSpace(txtReplace.Value) -- V3.0
if strNewVal:gsub("%s","") == strTxtVal:gsub("%s","") then return end
strWarning,strNewVal = StrAgeWarning(ptrItem,strTxtVal) -- Regenerate any Age Warnings and corrected Value
if strTxtVal ~= strNewVal then
if not TblOption.Tgl.WhSp then -- V3.0
strNewVal = strShowWhiteSpace(strNewVal)
end
local intCaretPos = txtReplace.CaretPos -- Maintain editing caret position and update Value
txtReplace.Value = strNewVal
txtReplace.CaretPos = intCaretPos
end
elseif strDataClass == "date" then -- Regenerate any Date Warnings
strWarning = StrDateWarning(ptrItem,strHideWhiteSpace(txtReplace.Value)) -- V3.0
elseif strDataClass == "text" then -- Generate any File issue Warning
strWarning = StrFileWarning(ptrItem,strHideWhiteSpace(txtReplace.Value),strHideWhiteSpace(strOldVal)) -- V4.0 -- V4.1
end
doWarning() -- V4.0
doChange(txtReplace) -- V3.0
end -- function txtReplace:valuechanged_cb
function txtReplace:caret_cb(lin,col,pos)
doCaret(txtReplace,pos) -- V3.0
end -- function txtReplace:caret_cb
iup_gui.ShowDialogue("User",dlgPerform,btnPerform,"Normal Keep") -- Popup dialogue that remembers its attributes & refreshes its size
strNewVal = strHideWhiteSpace(txtReplace.Value) -- Retrieve new Replacement text and Confirmation toggle values -- V3.0
TblOption.Tgl.Conf = ( tglConfirm.Value == "ON" )
dlgPerform:destroy()
return strButton, strNewVal
end -- function SearchReplacePrompt
-- Convert Media Relative Path to Absolute Path -- -- V3.9
function StrMediaRelToAbs(strPath,strFile)
-- strPath ~ Media folder path
-- strFile ~ Media file name
strFile = strFile:gsub("^[Mm]edia\\",function() return strPath.."Media\\" end)
return strFile
end -- function StrMediaRelToAbs
-- Perform Text Search and Optional Replace --
function DoSeekMatch(ptrItem,strDataClass)
-- ptrItem ~ Pointer to data item
-- strDataClass ~ Data Class of item
local strOldVal = fhGetValueAsText(ptrItem)
local strNewVal = ""
local strSeek = TblOption.Val.Seek -- V2.5 Val
local strSwap = TblOption.Val.Swap
local isOK = true -- V3.4
local function doManageString(strOldVal,strNewVal) -- V1.7 new function
if TblOption.Set.Mode == "Replace" then
if strDataClass == "richtext" then -- V3.2 -- Cater for richtext
local isRichText = false -- V3.7 -- Detect plain text v rich text
local arrLink = {}
local ptrLink = fhNewItemPtr()
ptrLink:MoveToFirstChildItem(ptrItem) -- Save any record link details -- V3.2
while ptrLink:IsNotNull() do
local strLink = fhGetTag(ptrLink)
if strLink:match("^_LINK_%u$") then
local intLink = fhGetValueAsInteger(fhGetItemPtr(ptrLink,'~._LKID'))
arrLink[intLink] = { Tag=strLink; Ptr=fhGetValueAsLink(ptrLink); }
end
isRichText = true -- Subsidiary _LINK_ or _FMT 1 tag found -- V3.7
ptrLink:MoveNext()
end
if isRichText then
isOK = fhSetValueAsRichText(ptrItem,fhNewRichText(strNewVal))-- Update richtext field -- isOK -- V3.4
else
isOK = fhSetValueAsText(ptrItem,strNewVal) -- Update plain text field -- V3.7
end
for intLink, dicLink in pairs(arrLink) do -- V3.3
ptrLink = fhCreateItem(dicLink.Tag,ptrItem) -- Reconstruct any record link details -- V3.2
if ptrLink:IsNotNull() then
fhSetValueAsLink(ptrLink,dicLink.Ptr)
ptrLink = fhCreateItem('_LKID',ptrLink)
if ptrLink:IsNotNull() then
fhSetValueAsInteger(ptrLink,intLink)
end
end
end
elseif strDataClass == "date" then -- V1.7 -- Cater for Date field
local newDate = fhNewDate(1999)
if strNewVal == "" then
newDate:SetNull() -- Null Date Object for empty string
else
newDate:SetValueAsText(strNewVal,true) -- Allow Date Phrase for Date Object
end
isOK = fhSetValueAsDate(ptrItem,newDate) -- isOK -- V3.4
else
if TblOption.Set.Data:match("FILE$") then -- Update linked Media file path? -- V2.9
local strPath = general.SplitFilename(fhGetContextInfo("CI_GEDCOM_FILE"))
local strOldFile = StrMediaRelToAbs(strPath,strOldVal) -- V3.9
local strNewFile = StrMediaRelToAbs(strPath,strNewVal) -- V3.9
isOK = general.FlgFileExists(strNewFile)
or not general.FlgFileExists(strOldFile)
if not isOK then -- Replacement Value file does not exist and Current Value file does exist -- V4.0
local _,_,strOldExt = general.SplitFilename(strOldVal)
local _,_,strNewExt = general.SplitFilename(strNewVal)
strOldExt = strOldExt:upper()
strNewExt = strNewExt:upper()
if strOldExt == strNewExt then
isOK = general.CopyFile(strOldFile,strNewFile) -- Copy linked Media file and alter absolute to relative path -- V4.0 -- V2.9
strNewVal = strNewVal:replace(strPath.."Media","Media")
end
end
end
if isOK then -- V3.9
isOK = fhSetValueAsText(ptrItem,strNewVal) -- For text values use Text object -- isOK -- V3.4
end
end
fhUpdateDisplay()
end
if not TblOption.Tgl.WhSp then -- V3.1
strOldVal = strShowWhiteSpace(strOldVal)
strNewVal = strShowWhiteSpace(strNewVal)
end
local ptrRecord = fhNewItemPtr() -- Save result set entries
ptrRecord:MoveToRecordItem(ptrItem)
if not isOK then -- Replacement failed -- V3.4
if IsMemoFail then
if 1 == iup_gui.WarnDialogue(" !! Replacement Failed ~ Old Value Retained !! "," The Result Set will list that error in the New Value column instead of the replacement: \n\n "..strNewVal.." \n\n Do you wish to inhibit these warning messages? \n\n","Yes","No") then
IsMemoFail = false
end
end
strNewVal = "!! Replacement Failed ~ Old Value Retained !!"
end
table.insert(TblRecord,ptrRecord:Clone())
table.insert(TblDataRef,TblOption.Set.Data)
table.insert(TblOldValue,strOldVal)
table.insert(TblNewValue,strNewVal)
table.insert(TblRefValue,ptrItem:Clone()) -- V3.1
end -- local function doManageString
if strDataClass == "richtext" then -- V3.1 -- Cater for richtext
strOldVal = fhGetValueAsRichText(ptrItem):GetText()
elseif strDataClass == "date" then -- V1.7 -- New conditional statement for Date field
--# strOldVal = fhGetItemText(ptrItem,fhGetTag(ptrItem)..":LONG") -- V2.5
strOldVal = fhGetValueAsDate(ptrItem):GetDisplayText("LONG") -- V3.8 -- Cope with templated source Date metafield
end
if TblOption.Tgl.Word then -- V2.1 revised to cater for successive repetitions of the seek word -- 1 Jan 2014
strNewVal = ("\02"..strOldVal.."\02"):gsub("(["..StrSP.."])","%1\02")-- Insert leading & trailing STX="\02" so first & last words match, and add STX="\02" to each white space/punctuation character so repeated words match
strNewVal = strNewVal:gsub(strSeek,strSwap):gsub("\02","") -- Replace words and remove all inserted STX="\02" characters
else
strNewVal = strOldVal:gsub(strSeek,strSwap) -- Replace any other cases
end
if strOldVal ~= strNewVal then
strNewVal = strNewVal:gsub("\01","") -- V2.0 Eliminate any SOH="\01" characters added by doPerformAction() to force a change
local strWarning = nil
if strDataClass == "age" then -- Obtain any Warning for Age field, with corrected text value
strWarning,strNewVal = StrAgeWarning(ptrItem,strNewVal)
elseif strDataClass == "date" then -- Obtain any Warning for Date field
strWarning = StrDateWarning(ptrItem,strNewVal)
elseif strDataClass == "text" then -- Obtain any Warning for File field -- V4.0
strWarning = StrFileWarning(ptrItem,strNewVal,strOldVal)
end
if TblOption.Tgl.Conf or strWarning then -- Confirmation required or Warning to report
local strButton = nil
strButton, strNewVal = SearchReplacePrompt(ptrItem,strOldVal,strNewVal,strWarning)
if strButton == TblOption.Set.Mode then
doManageString(strOldVal,strNewVal) -- V1.7 call new function
elseif strButton ~= "Skip" then
return false -- Process cancelled
end
else
TblOption.Set.Data = general.BuildDataRef(ptrItem)
doManageString(strOldVal,strNewVal) -- V1.7 call new function
end
end
return true
end -- function DoSeekMatch
function strScopeType(strScope) -- Extract Search Scope record type tag called from DoAllItems, SetScopeMode, btnSomeRecs:action -- V2.8
return strScope:match("^(_?%u+)")
end -- function strScopeType
-- Search All Data Items --
function DoAllItems()
local isContinue = true
local intItem = tonumber(TblOption.Set.Item) -- Number of items in database
local tblTgl = TblOption.Tgl -- Keyword array of Mode & Class & Tag boolean toggles
local strScope = TblOption.Lst.Scope -- Search Scope filter -- V2.3
local arrScope = { }
if #strScope == 0 then
for strType in iterate.RecordTypes() do -- Search Scope record type tags are every type with _PLAC first -- V3.2
if strType == "_PLAC" then
table.insert(arrScope,1,strType) -- This ensures Place record Place names get changed first
else
table.insert(arrScope,strType)
end
end
else
local strType = strScopeType(strScope) -- Search Scope record type tag -- V2.3 -- V2.4 -- V2.8
intItem = tonumber(TblOption.Set[strType]) or 0 -- Number of items in record type -- V2.3
table.insert(arrScope,strType)
strScope = "^"..strScope:plain().."%." -- Search Scope filter tag(s) pattern -- V2.3
end
local tblUser = iup_gui.DialogueAttributes("Main")
tblUser.X = tblUser.CoordX+100 -- Popup Progress Bar in same place as Main dialogue
tblUser.Y = tblUser.CoordY+150
progbar.Setup( tblUser )
if intItem > 5000 then
progbar.Start("Searching",intItem) -- Start Progress Bar if enough items in database/record type
end
intItem = 0 -- V2.3
for _, strType in ipairs( arrScope ) do -- Loop through all record types in Scope -- V3.2
local strRecRoot = strType.."." -- Record. tag for item Search Scope -- V2.3
local tblRecs = TblOption.Set.Recs
local useItem = true -- Is data item in Select Records filter? -- V2.8
progbar.Message("Searching "..strType.." Records")
for ptrItem in iterate.Items(strType) do -- Loop through all data items
if tblRecs then
local intRid = fhGetRecordId(ptrItem) -- Select Records filter exists so check Record Id -- V2.8
if intRid > 0 then useItem = tblRecs[intRid] end -- Adjust flag only on record level data items -- V2.8
end
if useItem then -- V2.8
local strChild = fhGetTag(ptrItem) -- Obtain child item Tag
local ptrParent = fhNewItemPtr()
ptrParent:MoveToParentItem(ptrItem) -- Obtain parent item pointer
local strParent = fhGetTag(ptrParent) -- Obtain parent item Tag -- V2.3
if strParent == strType then
strRecRoot = strType.."."..strChild.."." -- Record.Root. tags for Search Scope -- V2.3
end
if strRecRoot:match(strScope) then -- Item matches Search Scope filter -- V2.3
if fhIsFact(ptrParent) then
strParent = "~Fact." -- Parent is any Fact Tag
elseif fhGetDisplayText(ptrParent):match("^LDS %l%l") then
strParent = "~LDS." -- Parent is any LDS Ordinance
else
strParent = strParent.."." -- Otherwise use parent item Tag -- V2.3
ptrParent:MoveToParentItem(ptrParent) -- Prefix "~" to distinguish from Parent=Record
if ptrParent:IsNotNull() then strParent = "~"..strParent end
end
local strParentChild = strParent..strChild -- Parent.Child Tags
if fhIsAttribute(ptrItem) then strChild = "Attr" end -- Special case for all Attributes
local strDataClass,strChild = GetDataClass(ptrItem,strChild) -- Fix for metafields -- V3.4 -- V3.8
local strValueType = fhGetValueType(ptrItem) -- Only text & date fields are supported
if ( strValueType:match("text$") or strValueType == "date" or strValueType == "age" ) -- V3.1 match("text$") for "text" & "richtext" and add "age" for FH V7
and ( tblTgl[strChild] -- Child item Tag or V2.0 "Attr" ?
or tblTgl[strParentChild] -- V2.0 & V2.1 Parent.Child item Tags ?
or ( tblTgl[strDataClass] and -- V2.0 Class match and
tblTgl[strChild] == nil and -- Child and Parent.Child without any toggle option ?
tblTgl[strParentChild] == nil )
) then
isContinue = DoSeekMatch(ptrItem,strDataClass) -- Try string value match for matching data item type
end
end
if not isContinue then break end
if progbar.Stop() then break end
intItem = intItem + 1
if intItem > 99 then -- 1 Jan 2014
progbar.Step(100) -- Step progress bar every 100 items
intItem = 0
collectgarbage("step",0) -- Memory garbage collection to avoid 'Not responding' -- V3.4
end
end -- V2.8
end
end
progbar.Close()
end -- function DoAllItems
function CopyPreset(tblSource,tblTarget) -- Copy current options to/from chosen preset -- V2.5
if not tblSource then tblSource = {} end
if not tblTarget then tblTarget = {} end
for strHead, tblHead in pairs (tblSource) do
if type(strHead) == "string"
and strHead ~= "Set" then -- Only copy "Val", "Tgl", "Lst" dictionary tables
if not tblTarget[strHead] then
tblTarget[strHead] = {}
end
for strData, anyData in pairs (tblHead) do
tblTarget[strHead][strData] = anyData
end
end
end
end -- function CopyPreset
-- Manage Presets GUI -- -- V2.5
function ManagePresets()
local intCurrent = TblOption.Set.Curr -- Save currently selected Preset from Main GUI
local lstProject = iup.list { VisibleColumns=9; Multiple="Yes"; } -- Create GUI controls
local lstProgram = iup.list { VisibleColumns=9; Multiple="Yes"; } -- V2.6 Multiple="Yes"
local lstPhantom = iup.list { VisibleColumns=9; Multiple="Yes"; } -- V2.6 dummy for Export/Import
local btnSaveAll = iup.button{ Title=">> Save >>"; }
local btnLoadAll = iup.button{ Title="<< Load <<"; }
local btnExport = iup.button{ Title="Export" ; } -- V2.6 and other button names shortened
local btnImport = iup.button{ Title="Import" ; } -- V2.6
local btnMoveUp = iup.button{ Title="Move Up"; }
local btnMoveDn = iup.button{ Title="Move Down"; }
local btnUnlock = iup.button{ Title="Unlock" ; }
local btnDelete = iup.button{ Title="Delete" ; }
local lblPreset = iup.label { Title="New Preset Name :"; Alignment="ACENTER:ABOTTOM"; } -- V2.7
local txtPreset = iup.text { Value=""; }
local btnInsert = iup.button{ Title="Insert" ; }
local btnClone = iup.button{ Title="Clone" ; }
local btnRename = iup.button{ Title="Rename" ; }
local btnDestroy = iup.button{ Title="Finished"; action=function(self) return iup.CLOSE end; }
local frmProject = iup.frame { Title=" Project List "; lstProject; }
local frmProgram = iup.frame { Title=" Program List "; lstProgram; }
local boxManager = iup.vbox { Homogeneous="Yes"; btnSaveAll; btnLoadAll; btnExport; btnImport; btnMoveUp; btnMoveDn; btnUnlock; btnDelete; lblPreset; txtPreset; btnInsert; btnClone; btnRename; btnDestroy; } -- V2.6
local frmManager = iup.frame { Title=" Manage Presets "; boxManager; }
local boxPresets = iup.hbox { Homogeneous="Yes"; frmProject; frmManager; frmProgram; }
local dlgPresets = iup.dialog{ Title=iup_gui.Plugin..iup_gui.Version; Gap=iup_gui.Gap; Margin="4x4"; boxPresets; }
-- Set other GUI control attributes
for iupName, tblAttr in pairs ( {
-- Control = 1~FgColor ; 2~Font ; 3~Tip ;
[frmProject] = { iup_gui.Head; iup_gui.FontHead; };
[lstProject] = { iup_gui.Body; iup_gui.FontBody; "Select any local Project Presets in list"; };
[frmProgram] = { iup_gui.Head; iup_gui.FontHead; };
[lstProgram] = { iup_gui.Body; iup_gui.FontBody; "Select any global Program Presets in list"; };
[frmManager] = { iup_gui.Head; iup_gui.FontHead; };
[btnSaveAll] = { iup_gui.Warn; iup_gui.FontBody; "Copy selected Project Presets to Program List"; };
[btnLoadAll] = { iup_gui.Warn; iup_gui.FontBody; "Copy selected Program Presets to Project List"; };
[btnExport] = { iup_gui.Warn; iup_gui.FontBody; "Export any selected Presets to a File" ; }; -- V2.6
[btnImport] = { iup_gui.Warn; iup_gui.FontBody; "Import from a File to the Project List"; }; -- V2.6
[btnMoveUp] = { iup_gui.Safe; iup_gui.FontBody; "Move the selected Preset up the list" ; };
[btnMoveDn] = { iup_gui.Safe; iup_gui.FontBody; "Move the selected Preset down the list"; };
[btnUnlock] = { iup_gui.Risk; iup_gui.FontBody; "Unlock the selected Preset to allow deletion"; };
[btnDelete] = { iup_gui.Risk; iup_gui.FontBody; "Delete the selected Preset from the list"; };
[lblPreset] = { iup_gui.Body; iup_gui.FontBody; "Enter new Preset name for Insert, Clone, or Rename"; };
[txtPreset] = { iup_gui.Safe; iup_gui.FontBody; "Enter new Preset name for Insert, Clone, or Rename"; };
[btnInsert] = { iup_gui.Safe; iup_gui.FontBody; "Insert a blank Preset using New Preset Name"; };
[btnClone ] = { iup_gui.Safe; iup_gui.FontBody; "Clone selected Preset using New Preset Name"; };
[btnRename] = { iup_gui.Safe; iup_gui.FontBody; "Rename selected Preset using New Preset Name"; };
[btnDestroy] = { iup_gui.Risk; iup_gui.FontBody; "Finished Managing Presets"; };
} ) do
iupName.Expand = "Yes"
iupName.FgColor = tblAttr[1]
iupName.Font = tblAttr[2]
iupName.Tip = tblAttr[3]
iupName.TipBalloon = iup_gui.Balloon
end
local function intPresetChosen(lstPreset) -- Return number of selected items in Preset List
local _,intChosen = lstPreset.Value:replace("+","+")
return intChosen
end -- local function intPresetChosen
local function intPresetFirst(lstPreset) -- Return position of first selected item in Preset List
return lstPreset.Value:find("+",1,true) or 0
end -- local function intPresetFirst
local function strPresetValue(lstPreset,intPreset) -- Return list value for a Preset List
local strValue = ""
local intCount = tonumber(lstPreset.Count) or 0
for intValue = 1, math.max(intCount,intPreset or 0) do
if intValue == intPreset then -- Compose list control value string
strValue = strValue.."+"
else
strValue = strValue.."-"
end
end
if lstPreset == lstProject and intPreset then
intCurrent = intPreset -- Update current selected Project Preset
end
return strValue
end -- local function strPresetValue
local function isLocked(intValue,tblOption) -- Is current Preset locked?
if intValue > 0 then
return ( tblOption.Set.List[intValue].Lock == "ON" )
end
return false
end -- local function isLocked
local function newPreset(intValue,tblOption,strValue) -- Is new Preset value unique in chosen list
if intValue > 0 then
for intList, dicList in ipairs (tblOption.Set.List or {}) do
if dicList.Name == strValue then return false end -- No it already exists in list
end
end
return true
end -- local function newPreset
local function doDisableButtons() -- Set button controls inactive
for intName = 1, 20 do
local iupName = boxManager[intName]
if iupName == btnDestroy then break end -- Make each button inactive except btnDestroy
if iup.ClassMatch(iupName,"button") then iupName.Active = "No" end
end
end -- local function doDisableButtons
local function doEnableButtons() -- Set button controls active status
local intProjMax = tonumber(lstProject.Count) or 0
local intProgMax = tonumber(lstProgram.Count) or 0 -- Total count of presets in both lists
local intProject = intPresetChosen(lstProject)
local intProgram = intPresetChosen(lstProgram) -- Total selected presets in both lists
local intProj1st = intPresetFirst(lstProject)
local intProg1st = intPresetFirst(lstProgram) -- First selected preset in both lists
doDisableButtons()
if intProject > 0 then btnSaveAll.Active = "Yes" end -- If any selected preset then activate Save/Load buttons
if intProgram > 0 then btnLoadAll.Active = "Yes" end
if (intProject+intProgram) > 0 then btnExport.Active = "Yes" end -- Activate Export and Import buttons -- V2.6
btnImport.Active = "Yes"
if (intProject+intProgram) == 1 then -- If one selected preset in either list
if isLocked(intProj1st,TblOption)
or isLocked(intProg1st,ArrOption) then
btnUnlock.Active = "Yes" -- Activate Unlock button
else
btnDelete.Active = "Yes" -- Activate Delete button
end
if #txtPreset.Value > 0
and newPreset(intProj1st,TblOption,txtPreset.Value)
and newPreset(intProg1st,ArrOption,txtPreset.Value) then -- If new Preset name is valid
btnInsert.Active = "Yes"
btnClone .Active = "Yes" -- Activate Insert/Clone/Rename buttons
btnRename.Active = "Yes"
end
if intProg1st > 1 -- If not first preset selected then activate Move Up button
or intProj1st > 1 then btnMoveUp.Active = "Yes" end
if ( intProg1st > 0 and intProg1st < intProgMax ) -- If not last preset selected then activate Move Down button
or ( intProj1st > 0 and intProj1st < intProjMax ) then btnMoveDn.Active = "Yes" end
end
end -- local function doEnableButtons
function lstProject:action(strText,intItem,intState) -- Select presets from Project List
lstProgram.Value = strPresetValue(lstProgram) -- Deselect presets in Program List
intCurrent = intPresetFirst(lstProject) -- Update current selected Project Preset
doEnableButtons()
end -- function lstProject:action
function lstProgram:action(strText,intItem,intState) -- Select presets from Program List
lstProject.Value = strPresetValue(lstProject) -- Deselect presets in Project List
doEnableButtons()
end -- function lstProgram:action
local function doCopyOne(lstSource,tblSource,lstTarget,tblTarget) -- Copy one Preset from source to target list
local strPreset = nil -- V2.7
local intSource = intPresetFirst(lstSource)
if intSource > 0 then
local arrSource = tblSource.Set.List
local arrTarget = tblTarget.Set.List
local intTarget = 0
local strSource = arrSource[intSource].Name
for intList, dicList in ipairs (arrTarget) do -- Find source preset name in target list
if strSource == dicList.Name then
intTarget = intList
break
end
end
if intTarget == 0 then -- No target name found so insert new one
lstTarget.AppendItem = strSource
table.insert(arrTarget,{})
intTarget = #arrTarget
end
lstSource.Value = strPresetValue(lstSource)
lstTarget.Value = strPresetValue(lstTarget,intTarget) -- Select target preset as a visual cue
local intAns = 1
if arrTarget[intTarget].Lock == "ON" then
intAns = iup_gui.WarnDialogue("Preset Locked","\n Preset named '"..strSource.."' is Locked. \n\n Continue to overwrite this Preset? \n","Yes","No")
end
if intAns == 1 then
arrTarget[intTarget].Name = strSource
arrTarget[intTarget].Lock = arrSource[intSource].Lock
tblTarget[intTarget] = {}
CopyPreset(tblSource[intSource],tblTarget[intTarget]) -- Copy the preset options
strPreset = strSource -- V2.7
end
end
return strPreset -- V2.7
end -- local function doCopyOne
local function doCopyAll(lstSource,tblSource,lstTarget,tblTarget)
local arrPreset = {}
local strSource = lstSource.Value -- Remember original selected presets
local strTarget = lstTarget.Value
for intSource = 1, #tblSource do -- Loop through source list
if strSource:sub(intSource,intSource) == "+" then
lstSource.Value = strPresetValue(lstSource,intSource) -- Copy the preset options slowly
fhSleep(100,50)
table.insert(arrPreset,doCopyOne(lstSource,tblSource,lstTarget,tblTarget)) -- V2.7 arrPreset
fhSleep(100,50)
end
end
lstSource.Value = strSource -- Restore original selected presets
lstTarget.Value = strTarget
return arrPreset
end -- local function doCopyAll
function btnSaveAll:action() -- Save All presets button action
doDisableButtons()
doCopyAll(lstProject,TblOption,lstProgram,ArrOption)
doEnableButtons()
end -- function btnSaveAll:action
function btnLoadAll:action() -- Load All presets button action
doDisableButtons()
doCopyAll(lstProgram,ArrOption,lstProject,TblOption)
doEnableButtons()
end -- function btnLoadAll:action
local function doExport(lstSource,tblSource) -- Export selected Presets to File
if intPresetChosen(lstSource) > 0 then
local filedlg = iup.filedlg{ dialogtype="SAVE"; Title="Choose Presets Export File"; directory=iup_gui.PublicPath; file="Presets.fhdata"; extfilter="Preset files (*.fhdata)|*.fhdata|"; }
filedlg:popup(iup.CENTER,iup.CENTER)
if filedlg.status ~= "-1" then -- File chosen OK
local strFileName = filedlg.value
local strFolder, strExport = general.SplitFilename(strFileName)
if general.FlgFolderWrite(strFolder) then -- If chosen folder is writeable then all OK
fhSleep(200,100)
local tblExport = { Set={ List={}; }; }
doCopyAll(lstSource,tblSource,lstPhantom,tblExport) -- Save selected Presets to Export table in File
table.save(tblExport,strFileName)
lstPhantom.RemoveItem = "ALL"
fhSleep(200,100)
else
iup_gui.WarnDialogue("Export Failed","\n Export file '"..strExport.."' cannot be saved to folder. \n")
end
end
end
end -- local function doExport
function btnExport:action() -- Export presets button action -- V2.6
doDisableButtons()
doExport(lstProject,TblOption)
doExport(lstProgram,ArrOption)
doEnableButtons()
end -- function btnExport:action
local function doImport(strFileName,strImport) -- Load Import table from File -- V2.7
local tblImport, strError = table.load(strFileName)
if strError then
iup_gui.WarnDialogue("Import Error","\n Import File '"..strImport.."' Load Error: \n"..strError.."\n")
return nil
end
for intImport, arrImport in ipairs (tblImport) do
if not (arrImport.Tgl and arrImport.Lst and arrImport.Val)
or not (tblImport.Set and tblImport.Set.List)
or not (tblImport.Set.List[intImport] and tblImport.Set.List[intImport].Name and tblImport.Set.List[intImport].Lock) then
iup_gui.WarnDialogue("Import Error","\n Import File '"..strImport.."' Format Error. \n")
return nil
end
end
return tblImport
end -- local function doImport
function btnImport:action() -- Import presets button action -- V2.6
doDisableButtons()
local filedlg = iup.filedlg{ dialogtype="OPEN"; Title="Choose Presets Import File"; directory=iup_gui.PublicPath; extfilter="Preset files (*.fhdata)|*.fhdata|All files (*.*)|*.*|"; }
filedlg:popup(iup.CENTER,iup.CENTER)
if filedlg.status ~= "-1" then -- File chosen OK
local strFileName = filedlg.value
if general.FlgFileExists(strFileName) then
local strFolder, strImport = general.SplitFilename(strFileName)
local tblImport = doImport(strFileName,strImport) -- Load & check Import table from File -- V2.7
if tblImport then
fhSleep(200,100)
lstPhantom.Value = string.rep("+",#tblImport) -- Copy Import table to Project List -- V2.7
local arrPreset = doCopyAll(lstPhantom,tblImport,lstProject,TblOption)
fhSleep(200,100)
iup_gui.MemoDialogue("\n File '"..strImport.."' imported "..#arrPreset.." Presets. \n\n"..table.concat(arrPreset,"\n").."\n") -- V2.7
end
end
end
doEnableButtons()
end -- function btnImport:action
local function doMove(lstName,intDir,tblOption) -- Move preset up/down one position
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local tblPreset = tblOption.Set.List
local intNext1 = intValue + intDir -- New position is one above/below chosen preset
local strTemp = lstName[intValue]
lstName[intValue] = lstName[intNext1] -- Swap GUI list entries
lstName[intNext1] = strTemp
local tblTemp = tblPreset[intValue]
tblPreset[intValue] = tblPreset[intNext1] -- Swap associated table items
tblPreset[intNext1] = tblTemp
lstName.Value = strPresetValue(lstName,intNext1) -- Select the moved preset
tblTemp = {}
CopyPreset(tblOption[intValue],tblTemp)
CopyPreset(tblOption[intNext1],tblOption[intValue]) -- Swap the preset options
CopyPreset(tblTemp,tblOption[intNext1])
end
end -- local function doMove
function btnMoveUp:action() -- Move preset Up button action
doMove(lstProject,-1,TblOption)
doMove(lstProgram,-1,ArrOption)
doEnableButtons()
end -- function btnMoveUp:action
function btnMoveDn:action() -- Move preset Down button action
doMove(lstProject,1,TblOption)
doMove(lstProgram,1,ArrOption)
doEnableButtons()
end -- function btnMoveDn:action
local function doUnlock(lstName,tblOption) -- Unlock any preset from list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
tblOption.Set.List[intValue].Lock = "OFF"
end
end -- local function doUnlock
function btnUnlock:action() -- Unlock preset button action
doUnlock(lstProject,TblOption)
doUnlock(lstProgram,ArrOption)
doEnableButtons()
end -- function btnUnlock:action
local function doDelete(lstName,tblOption) -- Delete any preset from list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local arrPreset = tblOption.Set.List
lstName.RemoveItem = intValue -- Remove selected Preset from GUI list & associated table
table.remove(arrPreset,intValue)
local intCount = #arrPreset
for intItem = intValue, intCount do -- Move the preset options up to close gap
CopyPreset(tblOption[intItem+1],tblOption[intItem])
end
tblOption[intCount+1] = nil
tblOption[intCount+2] = nil
intValue = math.max(1,intValue-1) -- Select 1st preset or preset above removed preset
lstName.Value = strPresetValue(lstName,intValue)
end
end -- local function doDelete
function btnDelete:action() -- Delete preset button action
doDelete(lstProject,TblOption)
doDelete(lstProgram,ArrOption)
doEnableButtons()
end -- function btnDelete:action
function txtPreset:valuechanged_cb() -- Set buttons after preset name changed
doEnableButtons()
end -- function txtPreset:valuechanged_cb
local function doInsert(lstName,tblOption,btnAction) -- Insert/Clone new preset into list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local arrPreset = tblOption.Set.List
local strPreset = txtPreset.Value
intValue = intValue + 1
lstName["InsertItem"..intValue] = strPreset -- Insert new Preset name into GUI list & associated table
table.insert(arrPreset,intValue,{Name=strPreset;Lock="OFF";})
local intCount = #arrPreset
tblOption[intCount] = {}
for intItem = intCount, intValue, -1 do -- Move the preset options down
CopyPreset(tblOption[intItem-1],tblOption[intItem]) -- Leave chosen options in clone
end
if btnAction == btnInsert then
UserDefaultOptions(tblOption[intValue]) -- Reset the options for insert
end
txtPreset.Value = ""
lstName.Value = strPresetValue(lstName,intValue) -- Select inserted/cloned preset
end
end -- local function doInsert
function btnInsert:action() -- Insert preset button action
doInsert(lstProject,TblOption,btnInsert)
doInsert(lstProgram,ArrOption,btnInsert)
doEnableButtons()
end -- function btnInsert:action
function btnClone:action() -- Clone preset button action
doInsert(lstProject,TblOption,btnClone)
doInsert(lstProgram,ArrOption,btnClone)
doEnableButtons()
end -- function btnRename:action
local function doRename(lstName,tblOption) -- Rename any preset in list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local arrPreset = tblOption.Set.List
local strPreset = txtPreset.Value
lstName [intValue] = strPreset -- Put new Preset name in GUI list & associated table
arrPreset[intValue].Name = strPreset
txtPreset.Value = ""
lstName.Value = strPresetValue(lstName,intValue) -- Select renamed preset
end
end -- local function doRename
function btnRename:action() -- Rename preset button action
doRename(lstProject,TblOption)
doRename(lstProgram,ArrOption)
doEnableButtons()
end -- function btnRename:action
for intList, dicList in ipairs (TblOption.Set.List or {}) do -- Load the Project Preset list names
lstProject[intList] = dicList.Name
end
for intList, dicList in ipairs (ArrOption.Set.List or {}) do -- Load the Program Preset list names
lstProgram[intList] = dicList.Name
end
lstProject.Value = strPresetValue(lstProject,intCurrent) -- Select same Preset as in main GUI
lstProgram.Value = strPresetValue(lstProgram)
doEnableButtons()
iup_gui.ShowDialogue("Preset",dlgPresets,btnDestroy,"Normal Keep") -- Popup dialogue that remembers its attributes & refreshes its size
TblOption.Set.Curr = intCurrent -- Update currently selected Preset in Main GUI
end -- function ManagePresets
-- Main GUI Dialogue --
function GUI_MainDialogue()
--[[
TblOption.Set has these entries for global option settings :
Item Item count of database size for search progress bar
Date Date of last Item update to avoid recalculation run-time
List Droplist of the Project Preset names
Curr Current chosen Project Preset
Mode "Replace" = Search & Replace or "Report" = Search ONLY (not sticky)
Data Data reference for currently matched item (not sticky)
TblOption.Val has these values for search & replace strings :
Seek Search text string
Swap Replace text string
TblOption.Tgl and TblOption.Lst and other tables below may have these entries for "ON"/"OFF" toggles or combo drop list Tags :
Mixed case entries are Mode settings :
Text Plain Text Mode v. LUA Pattern Text Mode
Case Case Insensitive v. Case Sensitive
Word Whole Words v. Part Words
WhSp Hide v. Show white space symbols -- V3.0
Recs All Records v. Chosen Records -- V2.8
Scope Set Search Scope combo drop list -- V2.3
Conf Confirm Text Replacements
Attr Attribute Values enabled/disabled -- V2.0
Age Age Error checks enabled/disabled -- V2.1
Date Date Warnings enabled/disabled -- V1.7
Phrase Date Phrase Warnings enabled/disabled -- V2.2
Lower case entries are single Data Class names such as "text", "name", "place" and "longtext"
Upper case entries are single Gedcom Tag names such as "NICK", "FORM", "_PLAC" and "_EMAIL"
Upper case entries separated by a dot are Record parent & child Tag names such as "SOUR.TITL"
Upper case entries prefixed by a tilda "~" are non-Record parent & child such as "~DATA.TEXT"
These latter are used where similar Gedcom Tag names need to be differentiated :
NOTE.TEXT (Note record Text) versus ~_FIELD.TEXT & SOUR.TEXT & ~DATA.TEXT (Text from Source) all "longtext" or "richtext" in FH V7
INDI.NAME "name" (Individual Name) versus ~_FIELD.NAME & REPO.NAME & SUBM.NAME "text" (Metafield Name & Repository/Submitter record Name c.f. Source/Media record Title)
INDI.TITL "text" (Title Attribute) versus SOUR.TITL "longtext" & SOUR.ABBR "text" & OBJE.TITL "text" (Source/Media record Title)
Special variants are ~Fact.NOTE & ~Fact.DATE for any Fact parent tags, and ~LDS.NOTE & ~LDS.DATE for any LDS Ordinance parent tags.
--]]
local tblSet = TblOption.Set -- Local shortcuts to global option tables
local tblVal = TblOption.Val
local tblTgl = TblOption.Tgl
local tblLst = TblOption.Lst
local tblBasic = { } -- GUI basic filter toggle iup controls
local tblExtra = { } -- GUI extra filter toggle iup controls
local tblCombo = { } -- GUI combo filter drop list iup controls
local tblRelated = { -- Related GUI settings dependancies
-- Short Text fields: "text", "word", "name", "place", etc...
text = { "word"; "word2"; "latlong"; "enumword"; "wordlist"; };-- FORM = "word"; ROLE & TYPE = "word2"; LATLONG = "latlong"; QUAY, PEDI, etc = "enumword"; "wordlist"?; remainder are "text", except NAME/"name" & PLAC/"place" below -- V3.1
name = { "NPFX"; "NSFX"; "NICK"; "GIVN"; "_USED"; "SURN"; "SPFX"; "~NAME.FONE"; "~NAME.ROMN"; "~_FIELD.NAME"; }; -- V3.1 add FONE & ROMN -- V3.8 add ~_FIELD.NAME
PLAC = { "place"; "_PLAC"; "~PLAC.FONE"; "~PLAC.ROMN"; "~_FIELD.PLAC"; }; -- V3.1 add FONE & ROMN -- V3.8 add ~_FIELD.PLAC
RFN = { "RIN" ; };
FORM = { }; -- V2.0 FORM is "word" for OBJE.FORM & OBJE.FILE.FORM and PLAC.FORM
FILE = { "_FILE"; }; -- V2.9 add FILE
["~_FIELD.TEXT"]= { "~_FIELD.NAME"; "~_FIELD.DATE"; "~_FIELD.PLAC"; "~_FIELD.ENUM"; "~_FIELD.URL"; }; -- V3.1 -- "name", "date", "place", "text", "url"
PHON = { "EMAIL"; "WWW" ; "_WEB"; "_EMAIL"; }; -- V3.1 add EMAIL & WWW
ADR1 = { "ADR2" ; "ADR3"; "CITY"; "STAE"; "POST"; "CTRY"; }; -- V3.1 add ADR3
TEMP = { "AFN" ; "FAMF"; }; -- INDI.CONL.TEMP & SUBN.TEMP & INDI.AFN & SUBN.FAMF
_SRCT = { "_SRCT.NAME"; "_SRCT.COLL"; "_SRCT.CATG"; "_SRCT.SUBC"; "_SRCT.TITL"; "_SRCT.BIBL"; "_SRCT.FOOT"; "_SRCT.SHRT"; "~FDEF.NAME"; "~FDEF.CODE"; "~FDEF.TYPE"; "~FDEF.PROM"; }; -- V3.1 -- V4.1
-- Long Text fields: "longtext" or "richtext" in FH V7
NOTE2 = { "_NOTE"; "_NOTA"; "_SRCT.DESC"; "~FDEF.DESC" }; -- V3.1 add _NOTA & _SRCT.DESC & ~FDEF.DESC
["SOUR.TEXT"] = { "~DATA.TEXT"; };
["NOTE.TEXT"] = { "_RNOT.TEXT"; }; -- V3.1 add _RNOT.TEXT
ADDR = { "~_FIELD.ADDR"; }; -- V3.8 add ~_FIELD.ADDR metafield
-- Distinctive fields: "text" & "longtext" & "richtext" mix & "date"
["SOUR.TITL"] = { "SOUR.ABBR"; "OBJE.TITL"; "~FILE.TITL"; "REPO.NAME"; "_PLAC.TEXT"; "_PLAC.STAN"; "_SRCT.NAME"; "SUBM.NAME"; }; -- V3.1 add ~FILE.TITL & _PLAC.TEXT & _SRCT.NAME -- V3.5 add _PLAC.STAN
}
local function addFilterItem(arrFilt,strName,strTitle,strTip_T,strTip_L,...) -- Add a filter item -- V3.8
-- arrFilt ~ Filter table
-- strName ~ Name of filter
-- strTitle ~ Title of filter in GUI
-- strTip_T ~ Tip for toggle
-- strTip_L ~ Tip for drop list
-- ... ~ Drop list items
local dicFilt = { }
if strName == "Scope" then -- Search Scope special case
arrFilt.Name = strName
arrFilt.Tip_L = strTip_L
dicFilt = arrFilt
else
dicFilt = { Name=strName; Title=strTitle; Tip_T=strTip_T; Tip_L=strTip_L; }
end
for intItem, strItem in ipairs ({...}) do -- Set index value for each drop list item
table.insert(dicFilt,strItem)
if not dicFilt.Item then dicFilt.Item = {} end
strItem = strItem:gsub(" %(.-%)$","")
dicFilt.Item[strItem] = intItem
end
if strName ~= "Scope" then
table.insert(arrFilt,dicFilt)
if not arrFilt.Item then arrFilt.Item = {} end
arrFilt.Item[strName] = #arrFilt
end
return arrFilt[#arrFilt]
end -- local function addFilterItem
local tblFilters = { } -- GUI basic, extra & combo filter attributes -- V2.1
tblFilters.SearchScope = { fhFunc=fhIsFact; tblTag={}; Cols=20; } -- Search Scope special combo drop list -- V2.3
tblFilters.SearchScope.Item = { }
local tblScope = tblFilters.SearchScope
addFilterItem( tblScope, "Scope", nil, nil, "Set global search scope for Basic/Extra Filters",
"All Records & Events/Attributes", "Individual Records (INDI)", "Family Records (FAM)", "Note Records (NOTE)", "Source Records (SOUR)", "Repository Records (REPO)", "Multimedia Records (OBJE)", "Submitter Records (SUBM)", "Submission Records (SUBN)" )
local intScope = tblScope.Item["Submitter Records"]
if fhGetAppVersion() > 5 then -- FH V6 record scope -- V2.4/5
table.insert(tblScope,intScope,"Place Records (_PLAC)") -- Insert Place Records item before Submitter Records item
end
if fhGetAppVersion() > 6 then -- FH V7 record scope -- V3.1
table.insert(tblScope,intScope+1,"Research Note Records (_RNOT)") -- Insert these two after Place Records item
table.insert(tblScope,intScope+2,"Source Template Records (_SRCT)")
end
-- Name of filter= Title & Tip for toggle used in two filter tables;
tblFilters.name = { Title="Individual Names, Prefix, Suffix, Given, etc";
Tip_T="All Individual Primary and Alternate Name (NAME) fields including: \n (NPFX) Name Prefix, \n (NSFX) Name Suffix, \n (NICK) Nickname, \n (GIVN) Given Name, \n (_USED) Given Name Used, \n (SURN) Surname, \n (SPFX) Surname Prefix"; }
tblFilters.PLAC = { Title="All the Place fields"; -- V2.5 -- V3.8
Tip_T="Fact Place (PLAC) and To/From (_PLAC) and Source Event Place (PLAC) fields"; }
tblFilters.PAGE = { Title="Where Within Source fields in Citations";
Tip_T="Where Within Source (PAGE) fields in Citations"; }
tblFilters.NOTE2 = { Title="All the Note fields";
Tip_T="All the Note and Link/Note (NOTE/_NOTE) fields"; }
tblFilters.ADDR = { Title="All the Address fields"; -- V2.5 -- V3.8
Tip_T="Fact, Repository & Submitter Address (ADDR) fields"; }
tblFilters.TEXT = { Title="Text from Source fields";
Tip_T="Text from Source (TEXT) fields in Citations and Sources"; }
tblFilters.date = { Title="All the Date fields"; -- V2.5 moved
Tip_T="Fact, Citation Entry, Multimedia, and LDS Date (DATE/_DATE) fields"; } -- Fact DATE, Citation DATA.DATE, Multimedia OBJE._DATE, LDS Ordination BAPL/CONL/ENDL/SLGC/SLGS.DATE fields
if fhGetAppVersion() > 6 then -- FH V7 variants -- V3.1
tblFilters.name .Tip_T = tblFilters.name.Tip_T..", \n (FONE) Phonetic Name, \n (ROMN) Romanized Name, \n (~_FIELD.NAME) Templated Source Metafields" -- Source Metafield ~_FIELD.NAME -- V3.8
tblFilters.PLAC .Tip_T = tblFilters.PLAC.Tip_T..", \n Phonetic Place (FONE) and Romanized Place (ROMN) subfields, \n Templated Source Metafields (~_FIELD.PLAC)" -- Source Metafield ~_FIELD.PLAC -- V3.8
tblFilters.NOTE2.Title = "Note && Description fields"
tblFilters.NOTE2.Tip_T = "All local Note (NOTE) and Description (DESC) fields"
tblFilters.ADDR .Tip_T = tblFilters.ADDR.Tip_T:replace("Fact", "Fact, Source Metafield" ) -- Source Metafield ~_FIELD.ADDR -- V3.8
tblFilters.date .Tip_T = tblFilters.date.Tip_T:replace("Entry","Entry, Source Metafield") -- Source Metafield ~_FIELD.DATE
end
tblFilters.BasicShort = { } -- Basic Filter Short Text toggles -- V2.1
local arrFilter = tblFilters.BasicShort
-- Filter , Name , Title , Tip for toggle -- V3.8
addFilterItem( arrFilter , "name" , tblFilters.name.Title , tblFilters.name.Tip_T )
addFilterItem( arrFilter , "PLAC" , tblFilters.PLAC.Title , tblFilters.PLAC.Tip_T ) -- V2.5
addFilterItem( arrFilter , "PAGE" , tblFilters.PAGE.Title , tblFilters.PAGE.Tip_T )
addFilterItem( arrFilter , "text" , "All other Text fields", "All other Short Text and Long Text fields" )
tblFilters.BasicLong = { } -- Basic Filter Long Text toggles -- V2.1
local arrFilter = tblFilters.BasicLong
-- Filter , Name , Title , Tip for toggle -- V3.8
addFilterItem( arrFilter , "NOTE2" , tblFilters.NOTE2.Title , tblFilters.NOTE2.Tip_T )
addFilterItem( arrFilter , "ADDR" , tblFilters.ADDR.Title , tblFilters.ADDR.Tip_T ) -- V2.5
addFilterItem( arrFilter , "SOUR.TEXT", tblFilters.TEXT.Title , tblFilters.TEXT.Tip_T )
addFilterItem( arrFilter , "date" , tblFilters.date.Title , tblFilters.date.Tip_T ) -- V2.5 moved
tblFilters.ShortText = { } -- Extra Filter Short Text toggles & combo drop lists
local arrFilter = tblFilters.ShortText
-- Filter , Name , Title , Tip for toggle , Tip for drop list , Drop list -- V3.8
addFilterItem( arrFilter , "name" , tblFilters.name.Title , tblFilters.name.Tip_T )
local tblPlac = addFilterItem( arrFilter , "PLAC" , tblFilters.PLAC.Title , tblFilters.PLAC.Tip_T , "Select a Place field type" ,
"All the Place fields", "Fact Places (~Fact.PLAC)", "To/From Place (~Fact._PLAC)", "Source Event Place (~EVEN.PLAC)" ) -- V3.8 added Place options
if fhGetAppVersion() > 6 then
table.insert(tblPlac,"Phonetic Place (~PLAC.FONE)") -- FH V7 Add drop list Phonetic, Romanized & Source Metafield -- V3.8
table.insert(tblPlac,"Romanized Place (~PLAC.ROMN)")
table.insert(tblPlac,"Source Metafield (~_FIELD.PLAC)")
end
local tblAttr = addFilterItem( arrFilter , "Attr" , "Fact Attribute values" , "Any value for Fact Attributes" , "Select an Attribute fact" ,
"All the Attribute facts" ) tblAttr.fhFunc=fhIsAttribute tblAttr.tblTag={RESI=true;} -- Append all Attributes with values, so exclude Residence (RESI)
addFilterItem( arrFilter , "CAUS" , "Fact Cause fields" , "Fact Cause (CAUS) fields" )
addFilterItem( arrFilter , "AGNC" , "Fact && Source Responsible Agency fields" , "Fact and Source Responsible Agency (AGNC) fields" )
addFilterItem( arrFilter , "PAGE" , tblFilters.PAGE.Title , tblFilters.PAGE.Tip_T )
addFilterItem( arrFilter , "_TYPE" , "Generic Type fields in Source records" , "Generic Type (_TYPE) fields in Source records" )
if fhGetAppVersion() > 6 then
addFilterItem( arrFilter , "~_FIELD.TEXT", "Templated Source Metafields" , "Templated Source record (_FIELD) Metafields" , "Select a Templated Source Metafield" ,
"All Templated Source Metafields", "Text (~_FIELD.TEXT)", "Name (~_FIELD.NAME)", "Date (~_FIELD.DATE)", "Place (~_FIELD.PLAC)", "Address (~_FIELD.ADDR)", "Enum (~_FIELD.ENUM)", "URL (~_FIELD.URL)" ) -- V3.8
end
local tblType = addFilterItem( arrFilter , "TYPE" , "Type fields for Facts && Custom Id" , "Type (TYPE) fields for Facts and Custom Id" )
if fhGetAppVersion() > 6 then
tblType.Title = tblType.Title.." && Phonetic/Roman Names/Places" tblType.Tip_T = tblType.Tip_T.."\n and Phonetic or Romanized Names or Places"
end
addFilterItem( arrFilter , "REFN" , "Custom Id fields" , "Custom Id (REFN) fields" )
addFilterItem( arrFilter , "RFN" , "Permanent && Automated Record Number fields", "Permanent (RFN) and Automated (RIN) Record Number fields" )
addFilterItem( arrFilter , "FORM" , "Multimedia Format && Place Hierarchy fields", "Multimedia Format and Place Hierarchy (FORM) fields" ) -- V2.9
local tblFile = addFilterItem( arrFilter , "FILE" , "Multimedia Linked File fields && Media files", "Multimedia Linked File (FILE/_FILE) fields \n and associated Media path names" ) -- V2.9
if fhGetAppVersion() > 6 then
tblFile.Tip_T = tblFile.Tip_T:replace("/_FILE","")
end
addFilterItem( arrFilter , "_EXCL" , "Multimedia Exclude flags" , "Multimedia Exclude from Diagrams & Reports (_EXCL) flags" ) -- V3.1
addFilterItem( arrFilter , "_KEYS" , "Multimedia Keyword fields" , "Multimedia Keyword (_KEYS) fields" )
local tblPhon = addFilterItem( arrFilter , "PHON" , "Phone && Email && Website fields" , "All the Phone (PHON) and Email (_EMAIL) and Website (_WEB) fields" )
if fhGetAppVersion() > 6 then
tblPhon.Tip_T = tblPhon.Tip_T:replace("_EMAIL","EMAIL"):replace("_WEB","WWW")
end
local tblAdr1 = addFilterItem( arrFilter , "ADR1" , "Address Line1/2, City, State, Postcode && Country", "All address Line1 (ADR1), Line2 (ADR2), City (CITY), \n State (STAE), Postcode (POST) and Country (CTRY) fields" )
if fhGetAppVersion() > 6 then
tblAdr1.Title = tblAdr1.Title:replace("Line1/2","Line1/2/3") tblAdr1.Tip_T = tblAdr1.Tip_T:replace("(ADR2)","(ADR2), Line3 (ADR3)")
end
addFilterItem( arrFilter , "TEMP" , "LDS Temple && File Number && Filename fields", "LDS Temple Codes (TEMP), Ancestral File Number (AFN)\n and Family Filename (FAMF) fields" )
if fhGetAppVersion() > 6 then
addFilterItem( arrFilter , "_SRCT" , "Source Template record fields " , "Source Template record (_SRCT) short text fields" , "Select a Source Template record short text field" ,
"All Template short text fields", "Name (_SRCT.NAME)", "Collection (_SRCT.COLL)", "Category (_SRCT.CATG)", "Subcategory (_SRCT.SUBC)", "Bibliography (_SRCT.BIBL)", "Footnote (_SRCT.FOOT)", "Short Footnote (_SRCT.SHRT)", "Metafield Name (~FDEF.NAME)", "Metafield Code (~FDEF.CODE)", "Metafield Type (~FDEF.TYPE)", "Metafield Prompt (~FDEF.PROM)" )
end
local tblText = addFilterItem( arrFilter , "text" , "All other one line Short Text fields" , "All other one line Short Text fields including: \n (\"UDF\") Uncategorised Data Fields, \n (_SENT) Fact Customised Sentences, \n (QUAY) Citation Assessments, \n (ROLE) Citation Event Type Roles, \n (RELA) Association Relationships" )
if fhGetAppVersion() > 5 then -- FH V6 ShortText variants -- V2.4
tblText.Tip_T = tblText.Tip_T:replace("Event Type","Event Type & Witness"):replace("Relationships","Relationships, \n (LATLONG) Place Lat/Longitude Fields")
end
--[[ Uncategorised Data Fields (UDF),
Fact Customised Sentences (Fact._SENT,Fact._SHAR._SENT,Fact._SHAN._SENT),
Fact Witness Roles Titles (Fact._SHAR.ROLE,Fact._SHAN.ROLE),
Citation Assessment Types (Fact.SOUR.QUAY/_QUAY enumword),
Citation Event Type Roles (Fact.SOUR.EVEN.ROLE word2),
Association Relationships (INDI.ASSO.RELA),
Family Child Relationship (INDI.FAMC.PEDI/_PEDI enumword),
Place Lat/Longitude Field (_PLAC.MAP.LATLONG, latlong),
Submitter Record Language (SUBM.LANG),
Source Repository Identifications (SOUR.REPO.CALN), etc...
--]]
tblFilters.LongText = { } -- Extra Filter Long Text toggles & combo drop lists
local arrFilter = tblFilters.LongText
-- Filter , Name , Title , Tip for toggle , Tip for drop list , drop list -- V3.8
local tblNote = addFilterItem( arrFilter , "NOTE2" , tblFilters.NOTE2.Title , tblFilters.NOTE2.Tip_T , "Select a Note field type" ,
"All the Note fields", "Fact Notes (~Fact.NOTE)", "Individual (INDI.NOTE)", "Family (FAM.NOTE)", "Citation (~SOUR.NOTE)", "Source (SOUR.NOTE)", "Repository (REPO.NOTE)", "Multimedia (OBJE._NOTE)", "Link/Notes (OBJE.NOTE)", "LDS Ordination (~LDS.NOTE)", "Last Change (~CHAN.NOTE)" )
if fhGetAppVersion() > 5 then -- FH V6 Note variants -- V2.4/5
local intIndi = tblNote.Item["Individual"]
table.insert(tblNote,intIndi ,"Witness Notes (~_SHAR.NOTE)") -- Insert before Individual item
table.insert(tblNote,intIndi+1,"Witness Notes (~_SHAN.NOTE)")
table.insert(tblNote,#tblNote-1,"Place Notes (_PLAC.NOTE)") -- Insert as prepenultimate item
end
if fhGetAppVersion() > 6 then -- FH V7 Note variants -- V3.1
local intMedia = tblNote.Item["Multimedia"]
tblNote[intMedia+2] = "Media Note (OBJE.NOTE)" -- Needs +2 due to inserted Witness Notes items
tblNote[intMedia+3] = "Media Annotation (_NOTA)"
table.insert(tblNote,#tblNote-1,"Source Template (_SRCT.NOTE)") -- Insert as prepenultimate NOTE2 items
table.insert(tblNote,#tblNote-1,"Template Description (_SRCT.DESC)")
table.insert(tblNote,#tblNote-1,"Metafield Description (~FDEF.DESC)")
end
local tblAddr = addFilterItem( arrFilter , "ADDR" , tblFilters.ADDR.Title , tblFilters.ADDR.Tip_T , "Select an Address field type",
"All the Address fields", "Fact Addresses (~Fact.ADDR)", "Repository Address (REPO.ADDR)", "Submitter Address (SUBM.ADDR)" ) -- V3.8 added Address options
if fhGetAppVersion() > 6 then
table.insert(tblAddr,"Source Metafield (~_FIELD.ADDR)") -- FH V7 Add drop list Source Metafield -- V3.8
end
addFilterItem( arrFilter , "SOUR.TEXT", tblFilters.TEXT.Title , tblFilters.TEXT.Tip_T , "Select Citations or Sources",
"Both the field types", "Citation (~DATA.TEXT)", "Source (SOUR.TEXT)" )
addFilterItem( arrFilter , "AUTH" , "Author fields in Source records" , "Author (AUTH) fields in Source records" )
addFilterItem( arrFilter , "PUBL" , "Publication Info fields in Source records" , "Publication Info (PUBL) fields in Source records" )
addFilterItem( arrFilter , "SOUR2" , "Source Note fields" , "Source Note (SOUR) fields" )
addFilterItem( arrFilter , "NOTE.TEXT", "Note record Text fields" , "Note record Text (TEXT) fields" )
tblFilters.Distinct = { } -- Extra Filter Distinct toggles & combo drop lists
local arrFilter = tblFilters.Distinct
-- Filter , Name , Title , Tip for toggle , Tip for drop list , drop list -- V3.8
local tblTitl = addFilterItem( arrFilter , "SOUR.TITL", "Record Names/Titles" , "Source, Repository, Multimedia, Submitter Name/Title (ABBR/NAME/TITL) fields" , "Select Record name/title type" ,
"All the Record types", "Source Title (SOUR.TITL)", "Source Short (SOUR.ABBR)", "Repository (REPO.NAME)", "Multimedia (OBJE.TITL)", "Submitter (SUBM.NAME)" )
if fhGetAppVersion() > 5 then -- FH V6 SOUR.TITL
table.insert(tblTitl,#tblTitl,"Place Name (_PLAC.TEXT)") -- Insert as penultimate item
table.insert(tblTitl,#tblTitl,"Standardized (_PLAC.STAN)") -- Insert as penultimate item -- V3.5
tblTitl.Tip_T = tblTitl.Tip_T:gsub("(Submitter.*NAME)","Place, Standardized, \n %1/TEXT/STAN") -- V3.5
end
if fhGetAppVersion() > 6 then -- FH V7 SOUR.TITL -- V3.1
local intMedia = tblTitl.Item["Multimedia"]
tblTitl[intMedia] = "Multimedia (~FILE.TITL)"
table.insert(tblTitl,#tblTitl,"Source Template (_SRCT.NAME)") -- Insert as penultimate item
tblTitl.Tip_T = tblTitl.Tip_T:replace("Standardized","Standardized, Source Template") -- V3.5
end
addFilterItem( arrFilter , "age" , "Fact Age fields" , "Fact Age (AGE) fields" )
addFilterItem( arrFilter , "radAge_Mode" )
local tblDate = addFilterItem( arrFilter , "date" , tblFilters.date.Title , tblFilters.date.Tip_T , "Select a Date field type" ,
"All the Date fields", "Fact Dates (~Fact.DATE)", "Citation Entry (~DATA.DATE)", "LMO/Sort Date (~OBJE._DATE)", "Multimedia Date (OBJE._DATE)", "LDS Ordination (~LDS.DATE)" ) -- LDS Ordination BAPL/CONL/ENDL/SLGC/SLGS.DATE fields -- V3.0 added LMO/Sort Date (~OBJE._DATE)
if fhGetAppVersion() > 6 then -- FH V7 Dates -- V3.1
local intLMO = tblDate.Item["LMO/Sort Date"]
tblDate[intLMO] = "Local Media Date (~OBJE._DATE)"
table.insert(tblDate,intLMO,"Source Metafield (~_FIELD.DATE)")
end
addFilterItem( arrFilter , "radDateMode" )
addFilterItem( arrFilter , "radPhraseMode" ) -- V2.2
local function strComboTag(strTag) -- Combo drop list tag structure to ensure conformity, and structure must also be used in '2nd/3rd/4th... Item for drop list' above
if strTag then return " ("..strTag..")" end -- Used by doPopulateCombo() to set tag, and setComboValues() to match tag
return " %(([^()]+)%)$" -- Used by setScopeMode(), saveOptions(), and btnSomeRecs:action() to extract tag -- Fix to cope with custom facts like 'An (ODD) Fact (INDI.EVEN-AN_ODD_FACT)' -- V3.6
end -- local function strComboTag
local function doPopulateCombo(tblItem) -- Populate all combo drop list controls with items -- V2.1
local strName = tblItem.Name
local iupList = tblCombo[strName] -- Obtain the iup.list control via its name
iupList.AutoRedraw = "No"
iupList.Sort = "No"
for intTag = 1, #tblItem do -- Populate the combo drop list without Sort, as table items set order -- V2.3
iupList[intTag] = tblItem[intTag]
end
if tblItem.fhFunc then -- Populate the Scope/Attr list with appended FH Facts -- V2.3
local tblTag = tblItem.tblTag -- Dictionary & Array table of Fact Tags found -- V2.3
for intRec, strRec in ipairs ({"INDI";"FAM";}) do -- Find all Individual and Family Facts for combo drop list
progbar.Step() -- Step progress bar -- V2.6
for ptrRec in iterate.Records(strRec) do
for ptrFact in iterate.Facts(ptrRec) do
if tblItem.fhFunc(ptrFact) then -- Event/Attribute fact found -- V2.3
local strTag = fhGetTag(ptrFact)
if not tblTag[strTag] then -- Omit any Event/Attribute already found or excluded
tblTag[strTag] = true
table.insert(tblTag,fhCallBuiltInFunction("FactName",ptrFact)..strComboTag(strRec.."."..strTag))
end
end
end
if progbar.Stop() then error("\n Plugin Aborted. ") end
end
end
table.sort(tblTag) -- Sort the Facts alphabetically and append to the drop list
for intTag = 1, #tblTag do
iupList[ tonumber(iupList.Count) + 1 ] = tblTag[intTag] -- V3.1
end
end
iupList.AutoRedraw = "Yes"
end -- local function doPopulateCombo
local function setComboValues() -- Set values for combo drop list tags, called from btnDefault:action -- V2.1
for strName, iupList in pairs ( tblCombo ) do
local strTag = tblLst[strName] or ""
iupList.Value = 1 -- Default to 1st entry
if strTag ~= "" then
for intList = 2, iupList.Count do -- Search combo drop list items for matching tag
if iupList[intList]:matches(strComboTag(strTag)) then
iupList.Value = intList -- Select chosen entry
break
end
end
end
end
end -- local function setComboValues
local function setToolTip(iupItem1,iupItem2) -- Refresh control tooltip otherwise it vanishes in XP!
iupItem2 = iupItem2 or iupItem1
iupItem1.Tip = iupItem2.Tip
end -- local function setToolTip
local function iupToggle(tblItem,tblTab) -- Return toggle handle created from a filter table item above
local strName = tblItem.Name -- V2.3
local strValue = tblTgl[strName] or "ON"
local iupToggle = iup.toggle { Value=strValue; Title=tblItem.Title; Tip=tblItem.Tip_T; TipBalloon=iup_gui.Balloon; Expand="Yes"; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltip -- V2.3
iupToggle.TipDelay = 6000 + ( #tblItem.Tip_T * 20 ) -- Display larger tooltips for longer -- V2.3
tblTab[strName] = iupToggle -- Save toggle handle in tblBasic or tblExtra
return iupToggle
end -- local function iupToggle
local function iupList(tblItem) -- Return combo drop list handle created from a filter table item above
local strName = tblItem.Name -- V2.3
local intCols = tblItem.Cols or 14 -- Search Scope is wider than default for Extra Filter drop lists -- V2.3 was 9 -- V3.1 is 14
local iupList = iup.list { DropDown="Yes"; VisibleColumns=intCols; Visible_Items=20; Tip=tblItem.Tip_L; TipBalloon=iup_gui.Balloon; Expand="Yes"; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltip -- V2.3
tblCombo[strName] = iupList
doPopulateCombo(tblItem) -- Populate combo drop list with names & tags from filter table item above
return iupList
end -- local function iupList
local function doAppend(boxHandle,strEntry,...) -- Append controls to GUI box using a filter attributes entry above
local arg = {...} -- V3.1
local intArg = 1
local tblTab = tblExtra
if strEntry:match("Basic") then -- Choose tblBasic or tblExtra to save toggle handles
tblTab = tblBasic
end
for intItem, tblItem in ipairs ( tblFilters[strEntry] ) do -- For each filter table entry:
local iupItem = nil
if #tblItem > 0 then -- Append toggle and combo drop list -- V2.1 -- V2.3
iupItem = iup.hbox { iupToggle(tblItem,tblTab); iupList(tblItem); Homogeneous="Yes"; Gap="1"; Margin="2x0"; }
elseif tblItem.Title then -- V2.3
iupItem = iup.hbox { iupToggle(tblItem,tblTab); Margin="2x0"; }-- Append just a toggle
else
if arg[intArg] then
iupItem = arg[intArg] -- Append another control if supplied
intArg = intArg + 1
end
end
if iupItem then iup.Append(boxHandle,iupItem) end -- Append control to GUI box
local strName = tblItem.Name -- V2.3
if tblBasic[strName] and -- Adjust a Basic Filter to 3 State toggle
( strName == "text" or #tblItem > 0 ) then -- if "text" toggle or associated Extra Filter has combo drop list -- V2.3
local iupToggle = tblBasic[strName]
iupToggle["3State"] = "Yes" -- User can only set "ON"=1 or "OFF"=0 but Extra Filters tab can set "NOTDEF"=-1
iupToggle.action = function(self,intState) if intState < 0 then self.Value = "OFF" end setToolTip(self) end -- Refresh XP Tooltip
end
end
end -- local function doAppend
local function iupValue(strValue,tglA,tglB) -- Return toggle handle depending on toggle value
if strValue == "OFF" then
return tglB
else
return tglA
end
end -- local function iupValue
local function iupRadio(strValue,tglA,tglB) -- Return radio handle created from two toggles
return iup.radio { iup.hbox { tglA; tglB; Homogeneous="Yes"; }; Value=iupValue(strValue,tglA,tglB); }
end -- local function iupRadio
local function setAllToggles(strEntry,strMode) -- Set all Short/Long Text toggle options "ON"/"OFF"
for intItem, tblItem in ipairs ( tblFilters[strEntry] ) do
tblExtra[tblItem.Name].Value = strMode -- V2.3
if strMode == "ON" and tblCombo[tblItem.Name] then -- If toggle is ticked and combo drop list exists, -- V2.3
tblCombo[tblItem.Name].Value = 1 -- then reset combo drop list to default 1st entry
end
end
end -- local function setAllToggles
local function iupText(strVal) -- Return multiline text handle
return iup.text { Value=strVal; MultiLine="Yes"; AutoHide="Yes"; Formatting="Yes"; AddFormatTag=iup.user { Bulk="Yes"; CleanOut="Yes"; }; }
end -- local function iupText
local intItem = ( tonumber(TblOption.Set["INDI"]) or 0 ) + ( tonumber(TblOption.Set["FAM"]) or 0 )
if intItem > 9000 then -- Progress Bar for doPopulateCombo() function use of fhFunc -- V2.6
progbar.Setup()
progbar.Start("Initialisation",4) -- Initialisation Progress Bar, 2 x number of fhFunc in tblFilters above -- V2.6
progbar.Message("Finding Database Facts ~ Please Wait")
end
-- Create GUI controls
local lblCommands = iup.label { Title=" This plugin is intended for those scenarios that cannot be handled by these commands: \n 'Edit > Find and Replace...' and 'Tools > Work with Data > Places...' or 'Addresses...' \n It is NOT advisable to use those commands to amend Fact Place field names. "; Alignment="ACENTER"; } -- V3.2 -- V3.5
local lstSetScope = iupList ( tblFilters.SearchScope ) -- V2.3
local btnSomeRecs = iup.button { Title=" Select Records"; Padding="1x1"; Alignment=":ATOP"; } -- V2.8 -- V3.0
local lblSomeRecs = iup.label { Title="All possible records are selected "; } -- V2.8 -- V3.0
local frmSetScope = iup.frame { Title=" Search Scope "; iup.hbox { lstSetScope; btnSomeRecs; lblSomeRecs; Gap="14"; Margin="6x6"; }; } -- V3.0 Gap="19" -- V3.1 Gap="14"
local boxBasicShort = iup.vbox { iup.hbox { Homogeneous="Yes"; }; } doAppend( boxBasicShort, "BasicShort" )
local boxBasicLong = iup.vbox { iup.hbox { Homogeneous="Yes"; }; } doAppend( boxBasicLong , "BasicLong" )
local frmBasicFilt = iup.frame { Title=" Basic Filters "; iup.hbox { boxBasicShort; boxBasicLong; Homogeneous="Yes"; Margin="6x2"; }; }
local lblSeekText = iup.label { Title="Search: "; }
local txtSeekText = iupText ( tblSet.Seek ) txtSeekText.valuechanged_cb=function(self) setToolTip(self,lblSeekText) doChange(self) end txtSeekText.caret_cb=function(self,lin,col,pos) doCaret(self,pos) end -- V3.0
local lblSwapText = iup.label { Title="Replace: "; }
local txtSwapText = iupText ( tblSet.Swap ) txtSwapText.valuechanged_cb=function(self) setToolTip(self,lblSwapText) doChange(self) end txtSwapText.caret_cb=function(self,lin,col,pos) doCaret(self,pos) end -- V3.0
local tglTextPlain = iup.toggle { Title="Plain Text Mode"; }
local tglTextPatt = iup.toggle { Title="LUA Pattern Mode"; }
local radTextMode = iupRadio ( tblTgl.Text, tglTextPlain, tglTextPatt )
local tglCaseInse = iup.toggle { Title="Case Insensitive" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local tglCaseSens = iup.toggle { Title="Case Sensitive" ; action=function(self) setToolTip(self) end; }
local radCaseMode = iupRadio ( tblTgl.Case, tglCaseInse, tglCaseSens )
local tglWordWhole = iup.toggle { Title="Whole Words" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local tglWordPart = iup.toggle { Title="Part Words" ; action=function(self) setToolTip(self) end; }
local radWordMode = iupRadio ( tblTgl.Word, tglWordWhole, tglWordPart )
local tglWhSpHide = iup.toggle { Title="Hide White Space"; } -- V3.0
local tglWhSpShow = iup.toggle { Title="Show White Space"; } -- V3.0
local radWhSpMode = iupRadio ( tblTgl.WhSp, tglWhSpHide, tglWhSpShow )
local tglConfirm = iup.toggle { Title="Confirm the action for every item found"; Value=tblTgl.Conf or "ON"; }
local boxOptLeft = iup.hbox { iup.vbox { lblSeekText; lblSwapText; }; iup.vbox { txtSeekText; txtSwapText; }; Margin="6x0"; }
local boxOptRight = iup.vbox { radTextMode; radCaseMode; radWordMode; radWhSpMode; iup.hbox { tglConfirm; Margin="6x0"; }; Margin="6x2"; } -- V2.8 -- V3.0
local boxOptions = iup.hbox { boxOptLeft; boxOptRight; Homogeneous="Yes"; Margin="0x2"; }
local frmOptions = iup.frame { Title=" Search Criteria "; boxOptions; }
local lblSetPreset = iup.label { Title="Preset: "; } -- V2.5
local lstSetPreset = iup.list { Value=1; DropDown="Yes"; VisibleColumns=7; Visible_Items=20; } -- V2.5
local tglSetPreset = iup.toggle { Title="Lock"; action=function(self) setToolTip(self) end; } -- V2.5
local btnSetPreset = iup.button { Title="Manage Presets"; } -- V2.5
local boxSetPreset = iup.hbox { lblSetPreset; lstSetPreset; tglSetPreset; btnSetPreset; Gap="6"; Margin="6x6"; } -- V2.5
local frmSetPreset = iup.frame { Title=" Manage Presets "; boxSetPreset; } -- V2.5
local btnBasicRepl = iup.button { Title=" Search && Replace"; }
local btnBasicSeek = iup.button { Title=" Search ONLY "; }
local boxBasicBtn = iup.hbox { btnBasicRepl; btnBasicSeek; Homogeneous="Yes"; Gap="6"; Margin="6x6"; } -- V2.5
local frmBasicBtn = iup.frame { Title=" Search & Replace "; boxBasicBtn; } -- V2.5
local btnShortInc = iup.button { Title="Include All Short Text"; }
local btnShortExc = iup.button { Title="Exclude All Short Text"; }
local boxShortText = iup.vbox { iup.hbox { btnShortInc; btnShortExc; Homogeneous="Yes"; }; }
local frmShortText = iup.frame { Title=" Short Text Fields "; boxShortText; } doAppend( boxShortText, "ShortText" )
local btnLongInc = iup.button { Title="Include All Long Text"; }
local btnLongExc = iup.button { Title="Exclude All Long Text"; }
local boxLongText = iup.vbox { iup.hbox { btnLongInc; btnLongExc; Homogeneous="Yes"; }; }
local frmLongText = iup.frame { Title=" Long Text Fields "; boxLongText; } doAppend( boxLongText, "LongText" )
local tglAge_Inc = iup.toggle { Title="Age Warnings" ; action=function(self) setToolTip(self) end; TipBalloonTitle="BEWARE!"; TipBalloonTitleIcon="2"; }
local tglAge_Exc = iup.toggle { Title="No Age Warnings" ; action=function(self) setToolTip(self) end } -- Refresh XP Tooltips
local radAge_Mode = iupRadio ( tblTgl.Age, tglAge_Inc, tglAge_Exc )
local tglDateInc = iup.toggle { Title="Date Warnings" ; action=function(self) setToolTip(self) end; TipBalloonTitle="BEWARE!"; TipBalloonTitleIcon="2"; }
local tglDateExc = iup.toggle { Title="No Date Warnings" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local radDateMode = iupRadio ( tblTgl.Date, tglDateInc, tglDateExc )
local tglPhraseInc = iup.toggle { Title="Date Phrase Warnings" ; action=function(self) setToolTip(self) end; } -- V2.2
local tglPhraseExc = iup.toggle { Title="No Date Phrase Warnings" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local radPhraseMode = iupRadio ( tblTgl.Phrase, tglPhraseInc, tglPhraseExc )
local boxDistinct = iup.vbox { }
local frmDistinct = iup.frame { Title=" Distinctive Fields "; boxDistinct; } doAppend( boxDistinct, "Distinct", radAge_Mode, radDateMode, radPhraseMode ) -- V2.2
local btnExtraRepl = iup.button { Title=" Search && Replace"; }
local btnExtraSeek = iup.button { Title=" Search ONLY "; }
local boxExtraBtn = iup.hbox { btnExtraRepl; btnExtraSeek; Homogeneous="Yes"; Gap="6"; Margin="6x6"; }
local frmExtraBtn = iup.frame { Title=" Search & Replace "; boxExtraBtn; }
local btnDefault = iup.button { Title="Restore Defaults"; }
local btnSetFont = iup.button { Title="Set Window Fonts"; }
local btnGetHelp = iup.button { Title=" Help && Advice"; }
local btnDestroy = iup.button { Title="Close Plugin"; action=function() doDestroy() return iup.CLOSE end; }
local boxButtons = iup.hbox { btnDefault; btnSetFont; btnGetHelp; btnDestroy; Homogeneous="Yes"; Gap="5"; Margin="16x0"; }
local boxMajor = iup.vbox { lblCommands; frmSetScope; frmBasicFilt; frmOptions; iup.hbox { frmSetPreset; frmBasicBtn; Homogeneous="Yes"; }; Margin="0"; } -- V2.5 -- V3.0
local boxExtra = iup.hbox { frmShortText; iup.vbox { frmLongText; frmDistinct; frmExtraBtn; }; Homogeneous="Yes"; Margin="0"; } -- V2.5
-- Create the Tab controls layout
local tabControl = iup.tabs {
boxMajor; TabTitle0=" Major Options ";
boxExtra; TabTitle1=" Extra Filters ";
}
local dialogMain = iup.dialog { Title=iup_gui.Plugin..iup_gui.Version; Gap="1"; Margin="2x0"; iup.vbox { tabControl; boxButtons; }; }
local tblConfirm = { ON = "Confirmation prompt for every item found"; OFF = "Process all matching text unconditionally"; }
local tblTextTip = { ON = "Plain text"; OFF = "LUA pattern"; } -- V2.8
local tblActive = { ON = "Yes"; OFF = "No"; } -- V2.8
if fhGetAppVersion() > 6 then -- FH V7 IUP 3.28 -- V3.1
tabControl.TabPadding="8x4"
else -- FH V6 IUP 3.11 -- V3.1
tabControl.Padding="8x4"
end
progbar.Close() -- Close Progress Bar of Initialiation for Finding Record Facts -- V2.6
local function setTabMode(intTab) -- Transfer toggles according to chosen tab called from setControls, saveOptions, tabControl:tabchangepos_cb
intTab = intTab or IntTabPosn -- Default to current tab
if intTab < 2 then
local strTextValue = nil
for strName, anyValue in pairs ( tblExtra ) do -- Transfer the Extra Filters to the Basic Filters
local strExtraValue = tblExtra[strName].Value -- Extra Filter is "ON" or "OFF"
if strExtraValue == "ON"
and tblCombo[strName]
and tblCombo[strName].Value > "1" then -- Extra Filter is "ON" and combo drop list selects subset
strExtraValue = "NOTDEF" -- So Extra Filter is "NOTDEF"
end
if tblBasic[strName] and strName ~= "text" then -- Set each Basic Filter equal to Extra Filter (except "All other Text fields")
tblBasic[strName].Value = strExtraValue
else
strTextValue = strTextValue or strExtraValue -- If other Extra Filters all "ON" or all "OFF" then use that value
if strExtraValue ~= strTextValue then
strTextValue = "NOTDEF" -- Otherwise, with a mixture, use "NOTDEF" value
end
end
end
tblBasic["text"].Value = strTextValue -- Set the Basic "All other Text fields" to this value
elseif intTab == 2 then
for strName, anyValue in pairs ( tblExtra ) do -- Transfer the Basic Filters to the Extra Filters
local strBasicValue = tblBasic["text"].Value -- Default for Extra Filters uses Basic "All other Text fields"
if tblBasic[strName] then
strBasicValue = tblBasic[strName].Value -- But matching Extra Filter uses Basic Filter
end
if strBasicValue ~= "NOTDEF" then -- Unless the Basic Filter is "NOTDEF"
tblExtra[strName].Value = strBasicValue
if tblCombo[strName] and strBasicValue == "ON" then -- Filter is "ON" so reset combo drop list to 1st entry
tblCombo[strName].Value = 1
end
end
end
end
end -- local function setTabMode
local function setTextMode() -- Set Text Case & Word Modes called from setControls, tglTextPlain:action
tglTextPlain.Tip = tglTextPlain.Tip
tglTextPatt.Tip = tglTextPatt.Tip -- Refresh XP Tooltips
radCaseMode.Active = tblActive[tglTextPlain.Value] -- Plain Text Mode enables Case & Word modes -- V2.8
radWordMode.Active = tblActive[tglTextPlain.Value] -- LUA Pattern Mode disables Case & Word modes -- V2.8
lblSeekText.Tip = tblTextTip[tglTextPlain.Value].." string to search for"
txtSeekText.Tip = lblSeekText.Tip
lblSwapText.Tip = tblTextTip[tglTextPlain.Value].." string to substitute"
txtSwapText.Tip = lblSwapText.Tip
end -- local function setTextMode
local function setWhiteSpaceMode() -- Set Seek & Swap text White Space mode on/off -- V3.0
tglWhSpHide.Tip = tglWhSpHide.Tip
tglWhSpShow.Tip = tglWhSpShow.Tip -- Refresh XP Tooltips
tblTgl.WhSp = tglWhSpHide.Value
if tglWhSpHide.Value == "ON" then
txtSeekText.Value = strHideWhiteSpace(txtSeekText.Value)
txtSwapText.Value = strHideWhiteSpace(txtSwapText.Value)
else
txtSeekText.Value = strShowWhiteSpace(txtSeekText.Value)
txtSwapText.Value = strShowWhiteSpace(txtSwapText.Value)
end
end -- local function setWhiteSpaceMode
local function setWarnMode() -- Set Age & Date Warning Mode called from setControls, saveOptions, tblExtra.age:action, tblExtra.date:action, tabControl:tabchangepos_cb
tblExtra.age.Tip = tblExtra.age.Tip
tblExtra.date.Tip = tblExtra.date.Tip -- Refresh XP Tooltips
radAge_Mode.Active = tblActive[tblExtra.age.Value] -- Age Fields included/excluded enables/disables Warning mode -- V2.8
radDateMode.Active = tblActive[tblExtra.date.Value] -- Date Fields included/excluded enables/disables Warning mode -- V2.8
radPhraseMode.Active = tblActive[tblExtra.date.Value]
end -- local function setWarnMode
local function doMediaWarning() -- Warn of using "Multimedia Linked File fields & Media files" -- V4.0
local tblWarning = {
"'Multimedia Linked File fields && Media files'";
"This filter tries to sync each File link field and Windows file path to avoid broken links.";
"So linked Windows file paths may get changed wherever they exist on disc.";
"Beware of the impact on Windows files if they are shared with other products.";
"Changes to Windows files will NOT be reversed by Edit > Undo Plugin Updates.";
"See a full explanation in the Help && Advice page for the Extra Filters tab.";
" "; }
iup_gui.MemoDialogue( table.concat(tblWarning," \n\n ") )
end -- local function doMediaWarning()
local dicRecName = { INDI="Individual (INDI)"; FAM="Family (FAM)"; NOTE="Note (NOTE)"; SOUR="Source (SOUR)"; REPO="Repository (REPO)"; OBJE="Multimedia (OBJE)"; _PLAC="Place (_PLAC)"; _RNOT="Research Note (_RNOT)"; _SRCT="Source Template (_SRCT)"; SUBM="Submitter (SUBM)"; SUBN="Submission (SUBN)"; } -- V3.1
local function setScopeMode() -- Set Search Scope called from setControls, lstSetScope:action, btnSomeRecs:action -- V2.8
local strSize = "All "
local strType = "possible"
local strText = " records are selected "
if tblSet.Recs then
strSize = tblSet.Recs.Size or "All " -- Number of Record Id selected
if strSize == "1 " then strText = " record is selected " end
end
if lstSetScope.Value == "1" then
btnSomeRecs.Active = "No" -- Set Select Records button inactive when all records chosen
else
btnSomeRecs.Active = "Yes" -- Set Select Records button active and identify record type
strType = dicRecName[strScopeType(lstSetScope[lstSetScope.Value]:match(strComboTag()))] or "Error" -- Catch error -- V3.6
end
lblSomeRecs.Title = strSize..strType..strText
end -- local function setScopeMode
local function setControls() -- Reset GUI control values called from lst/tgl/btnSetPreset:action, btnDefault:action
local intCurr = tonumber(tblSet.Curr)
if TblOption[intCurr] then
CopyPreset(TblOption[intCurr],TblOption) -- Load the Preset database into current settings -- V2.5
end
for intList, dicList in ipairs (tblSet.List) do
lstSetPreset[intList] = dicList.Name -- Load the Preset database names into droplist -- V2.5
end
local arrFrame = {frmOptions;frmBasicFilt;frmShortText;frmLongText;frmDistinct;btnDefault;}
lstSetPreset.Value = intCurr
tglSetPreset.Value = tblSet.List[intCurr].Lock or "OFF" -- Enable/disable frames depending on Preset Lock -- V2.5
if tglSetPreset.Value == "OFF" then
for intFrame, iupFrame in ipairs (arrFrame) do iupFrame.Active = "Yes" end
else
for intFrame, iupFrame in ipairs (arrFrame) do iupFrame.Active = "No" end
end
txtSeekText.Value = tblVal.Seek
txtSwapText.Value = tblVal.Swap
txtSeekText.AddFormatTag = iup.user { Bulk="Yes"; CleanOut="Yes"; FgColor=iup_gui.Safe; } -- Special Multiline Formatted Text case!!!
txtSwapText.AddFormatTag = iup.user { Bulk="Yes"; CleanOut="Yes"; FgColor=iup_gui.Safe; }
tglConfirm.Value = tblTgl.Conf
tglConfirm.Tip = tblConfirm[tglConfirm.Value]
radTextMode.Value = iupValue(tblTgl.Text ,tglTextPlain,tglTextPatt)
radCaseMode.Value = iupValue(tblTgl.Case ,tglCaseInse ,tglCaseSens)
radWordMode.Value = iupValue(tblTgl.Word ,tglWordWhole,tglWordPart)
radWhSpMode.Value = iupValue(tblTgl.WhSp ,tglWhSpHide ,tglWhSpShow) -- V3.0
radAge_Mode.Value = iupValue(tblTgl.Age ,tglAge_Inc ,tglAge_Exc )
radDateMode.Value = iupValue(tblTgl.Date ,tglDateInc ,tglDateExc )
radPhraseMode.Value= iupValue(tblTgl.Phrase,tglPhraseInc,tglPhraseExc) -- V2.2
for strName, iupValue in pairs ( tblExtra ) do
tblExtra[strName].Value = tblTgl[strName] or "ON"
end
dialogMain:map() -- Needed to ensure Attributes combo drop list is sorted before setComboValues
setComboValues() -- Set values for matching combo drop list tags -- V2.1
setTabMode() -- Set toggles for current tab, must come after setComboValues()
setTextMode() -- Set Text Case & Word Modes depending on Plain Text/LUA Pattern mode
setWhiteSpaceMode() -- Set Seek & Swap text white space symbols on or off -- V3.0
setWarnMode() -- Set Date & Age Warning mode depending on Date & Age toggle settings
setScopeMode() -- Set Select Records mode and update records selected status -- V2.8
end -- local function setControls
-- Assign GUI control attributes -- V2.1
local tblControls = { { "Font"; "FgColor"; "Expand"; "Tip", {"TipBalloon";"Balloon"}; { "help_cb";function() iup_gui.HelpDialogue(IntTabPosn) end }; "action"; setControls; };
[tabControl ] = { "FontHead"; "Head"; "Yes" ; "Select 'Major Options' or 'Extra Filters'"; };
[frmSetScope] = { "FontHead"; "Head"; "Horizontal"; }; -- V3.0
[boxBasicShort] = { "FontBody"; "Safe"; "Yes"; };
[boxBasicLong] = { "FontBody"; "Safe"; "Yes"; };
[frmBasicFilt] = { "FontHead"; "Head"; "Yes"; };
[boxOptions ] = { "FontBody"; "Safe"; "Yes"; };
[frmOptions ] = { "FontHead"; "Head"; "Yes"; };
[boxSetPreset] = { "FontBody"; "Safe"; "Yes"; }; -- V2.5
[frmSetPreset] = { "FontHead"; "Head"; "Horizontal"; }; -- V2.5
[boxBasicBtn] = { "FontBody"; "Safe"; "Yes"; }; -- V2.5
[frmBasicBtn] = { "FontHead"; "Head"; "Horizontal"; }; -- V2.5
[boxShortText] = { "FontBody"; "Safe"; "Yes"; };
[frmShortText] = { "FontHead"; "Head"; "Yes"; };
[boxLongText] = { "FontBody"; "Safe"; "Yes"; };
[frmLongText] = { "FontHead"; "Head"; "Yes"; };
[boxDistinct] = { "FontBody"; "Safe"; "Yes"; };
[frmDistinct] = { "FontHead"; "Head"; "Yes"; };
[boxExtraBtn] = { "FontBody"; "Safe"; "Yes"; }; -- V2.5
[frmExtraBtn] = { "FontHead"; "Head"; "Horizontal"; }; -- V2.5
[lblCommands] = { "FontHead"; "Risk"; "Yes" ; "The standard commands may offer a simpler solution than this plugin "; }; -- V3.2
[lstSetScope] = { "FontBody"; "Safe"; "Yes" ; "Set global search scope for Basic/Extra Filters"; }; -- V3.0
[btnSomeRecs] = { "FontBody"; "Safe"; "Horizontal"; "Select records to include in search\nwhere type matches Search Scope"; }; -- V2.8
[lblSomeRecs] = { "FontBody"; "Body"; "Yes" ; "Selected records to include in Search Scope"; }; -- V2.8
[lblSeekText] = { "FontBody"; "Body"; "Vertical" ; "Plain text string to search for"; };
[txtSeekText] = { "FontBody"; "Safe"; "Yes" ; "Plain text string to search for"; };
[lblSwapText] = { "FontBody"; "Body"; "Vertical" ; "Plain text string to substitute"; };
[txtSwapText] = { "FontBody"; "Safe"; "Yes" ; "Plain text string to substitute"; };
[tglTextPlain] = { "FontBody"; "Safe"; "Yes" ; "Plain text strings"; };
[tglTextPatt] = { "FontBody"; "Safe"; "Yes" ; "LUA pattern strings\nSee 'Help and Advice' for details"; };
[tglCaseInse] = { "FontBody"; "Safe"; "Yes" ; "Case insensitive text search"; };
[tglCaseSens] = { "FontBody"; "Safe"; "Yes" ; "Case sensitive text search"; };
[tglWordWhole] = { "FontBody"; "Safe"; "Yes" ; "Whole words are delimited by either\nspace, tab, newline, punctuation, or field edge"; };
[tglWordPart] = { "FontBody"; "Safe"; "Yes" ; "Part words may be any text"; };
[tglWhSpHide] = { "FontBody"; "Safe"; "Yes" ; "Hide white space symbols"; }; -- V3.0
[tglWhSpShow] = { "FontBody"; "Safe"; "Yes" ; "Show white space symbols"; }; -- V3.0
[tglConfirm ] = { "FontBody"; "Safe"; "Yes" ; tblConfirm[tglConfirm.Value] ; function() tglConfirm.Tip = tblConfirm[tglConfirm.Value] end; };
[lblSetPreset] = { "FontBody"; "Body"; "Vertical" ; "Choose the current Preset settings"; }; -- V2.5
[lstSetPreset] = { "FontBody"; "Safe"; "Yes" ; "Choose the current Preset settings"; }; -- V2.5
[tglSetPreset] = { "FontBody"; "Safe"; "Vertical" ; "Lock the Preset to prevent changes"; }; -- V2.5
[btnSetPreset] = { "FontBody"; "Safe"; "Yes" ; "Manage the Preset Settings database"; }; -- V2.5
[btnBasicRepl] = { "FontHead"; "Safe"; "Yes" ; "Start the Search and Replace process"; };
[btnBasicSeek] = { "FontHead"; "Safe"; "Yes" ; "Start the Search only process"; };
[btnShortInc] = { "FontBody"; "Safe"; "Yes" ; "Include all one line Short Text fields" ; function() setAllToggles("ShortText","ON") doMediaWarning() end; }; -- V4.0
[btnShortExc] = { "FontBody"; "Risk"; "Yes" ; "Exclude all one line Short Text fields" ; function() setAllToggles("ShortText","OFF") end; };
[btnLongInc ] = { "FontBody"; "Safe"; "Yes" ; "Include all multi-line Long Text fields" ; function() setAllToggles("LongText","ON") end; };
[btnLongExc ] = { "FontBody"; "Risk"; "Yes" ; "Exclude all multi-line Long Text fields" ; function() setAllToggles("LongText","OFF") end; };
[tglAge_Inc ] = { "FontBody"; "Risk"; "Yes" ; "BEWARE: Affects record 'Updated' date\neven if the Age fields are skipped ! !"; };
[tglAge_Exc ] = { "FontBody"; "Safe"; "Yes" ; "Age Warnings disabled"; };
[tglDateInc ] = { "FontBody"; "Risk"; "Yes" ; "BEWARE: Affects record 'Updated' date\neven if the Date fields are skipped ! !"; };
[tglDateExc ] = { "FontBody"; "Safe"; "Yes" ; "Date Warnings disabled"; };
[tglPhraseInc] = { "FontBody"; "Risk"; "Yes" ; "Date Phrase Warnings enabled"; }; -- V2.2
[tglPhraseExc] = { "FontBody"; "Safe"; "Yes" ; "Date Phrase Warnings disabled"; }; -- V2.2
[btnExtraRepl] = { "FontHead"; "Safe"; "Yes" ; "Start the Search and Replace process"; };
[btnExtraSeek] = { "FontHead"; "Safe"; "Yes" ; "Start the Search only process"; };
[btnDefault ] = { "FontBody"; "Safe"; "Horizontal"; "Restore default Settings for Options and Window positions and sizes"; };
[btnSetFont ] = { "FontBody"; "Safe"; "Horizontal"; "Choose user interface window font style"; };
[btnGetHelp ] = { "FontBody"; "Safe"; "Horizontal"; "Obtain online Help and Advice from the Plugin Store"; };
[btnDestroy ] = { "FontBody"; "Risk"; "Horizontal"; "Close the Plugin"; };
}
iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes -- V2.1
local function saveOptions() -- Save all GUI settings called from doSearchReplace, doSearchOnly, lst/tgl/btnSetPreset:action, btnSetFont:action, btnDestroy:action
if IntTabPosn < 2 then
setTabMode(2) -- Transfer Basic Filters to Extra Filters if necessary
setWarnMode()
end
for strName, anyValue in pairs ( tblTgl ) do
if not tblExtra[strName] then
tblTgl[strName] = nil -- Clear the non-GUI Class and Tag toggles
end
end
for strName, anyValue in pairs ( tblExtra ) do
tblTgl[strName] = tblExtra[strName].Value -- Save all the GUI Class and Tag and "Attr" toggle filters
end
for strName, anyValue in pairs ( tblCombo ) do
local strText = tblCombo[strName][tblCombo[strName].Value]
tblLst[strName] = strText:match(strComboTag()) or "" -- Save all the GUI combo drop list tag filters
end
tblVal.Seek = strHideWhiteSpace(txtSeekText.Value) -- Save text strings -- V2.5 -- Hide white space symbols -- V3.0
tblVal.Swap = strHideWhiteSpace(txtSwapText.Value)
tblSet.Mode = nil -- Mode and Data Ref are not saved
tblSet.Data = nil
tblTgl.Conf = tglConfirm.Value -- Save all mode toggles
tblTgl.Text = tglTextPlain.Value
tblTgl.Case = tglCaseInse.Value
tblTgl.Word = tglWordWhole.Value
tblTgl.WhSp = tglWhSpHide.Value -- V3.0
tblTgl.Age = tglAge_Inc.Value
tblTgl.Date = tglDateInc.Value
tblTgl.Phrase= tglPhraseInc.Value -- V2.2
local intCurr = tonumber(tblSet.Curr)
tblSet.List[intCurr].Lock = tglSetPreset.Value
if not TblOption[intCurr] then TblOption[intCurr] = {} end -- V2.5
CopyPreset(TblOption,TblOption[intCurr]) -- Save the Preset database from current settings -- V2.5
SaveSettings() -- Save sticky data settings
end -- local function saveOptions
function tglTextPlain:action(intState) -- Action for Plain Text Mode v LUA Pattern Mode radio toggle
setTextMode()
end -- function tglTextPlain:action
function tglWhSpHide:action(intState) -- Action for Hide/Show White Space symbols radio toggle -- V3.0
setWhiteSpaceMode()
end -- function tglWhSpHide:action
function lstSetScope:action(strText,intItem,intState) -- Action for Search Scope drop list -- V2.8
lstSetScope.Tip = lstSetScope.Tip -- Refresh XP Tooltip
if intState == 1 then
tblSet.Recs = nil -- Enable all Record Id
setScopeMode() -- Make the Select Records button active depending on Search Scope droplist
end
end -- lstSetScope:action
function btnSomeRecs:action() -- Action for Select Records button -- V2.8
dialogMain.Active="No"
local strType = strScopeType(lstSetScope[lstSetScope.Value]:match(strComboTag())) or "INDI"
local tblRecs = fhPromptUserForRecordSel(strType) -- Select Record Id for Search Scope record type tag
tblSet.Recs = nil -- Enable all Record Id
if #tblRecs > 0 then
tblSet.Recs = {}
tblSet.Recs.Size = tostring(#tblRecs).." " -- Save number of Record Id selected
for intRec, ptrRec in ipairs (tblRecs) do
tblSet.Recs[fhGetRecordId(ptrRec)] = true -- Tabulate each Record Id selected
end
end
setScopeMode() -- Update status message of records selected
dialogMain.Active="Yes"
dialogMain.BringFront="Yes"
end -- function btnSomeRecs:action
function tblExtra.age:action(intState) -- Action for Age fields toggle
setWarnMode()
end -- function tblExtra.age:action
function tblExtra.date:action(intState) -- Action for Date fields toggle
setWarnMode()
end -- function tblExtra.date:action
function tblExtra.FILE:action(intState) -- Action for Multimedia Linked File fields & Media files toggle -- V4.0
if intState == 1 then
doMediaWarning()
end
end -- function tblExtra.FILE:action
local function doPerformAction() -- Perform action for both Search & Replace and Search ONLY button
if tblVal.Seek:match("\n") or tblVal.Swap:match("\n") then -- Check newline characters are only used with Long Text fields -- V2.1
local strToggle = "OFF"
for intItem, tblItem in ipairs ( tblFilters.ShortText ) do
if tblTgl[tblItem.Name] == "ON" then -- Must check before true/false adjustment below as main toggle may get disabled -- V2.4
strToggle = "ON" -- Short Text option selected
break
end
end
if strToggle == "ON" -- Short Text or Record Title or Age or Date selected?
or tblTgl["SOUR.TITL"] == "ON" -- Source Title is Long Text, but others are Short Text, and Source Title unlikely to have newlines
or tblTgl.age == "ON"
or tblTgl.date == "ON" then
iup_gui.MemoDialogue("\n Newline characters in Search &&/or Replace text \n are reserved exclusively for Long Text Fields, \n such as Note, Fact Address, Text from Source. \n")
return false
end
end
for strName, strValue in pairs ( tblTgl ) do -- TblOptions can be altered as SaveSettings() is not now called except via saveOptions()
tblTgl[strName] = ( strValue == "ON" ) -- Swap "ON" to true, and "OFF" to false, in all toggle options
end
for strSource, tblTarget in pairs ( tblRelated ) do
for _, strTarget in ipairs ( tblTarget ) do -- Settings for related target Classes & Tags from source options
tblTgl[strTarget] = tblTgl[strSource]
end
end
for strName, anyValue in pairs ( tblCombo ) do -- Adjust true/false settings for related combo drop list tags
if tblTgl[strName] and tblLst[strName] ~= "" then -- Matching extra toggle option is ticked and combo drop list names a tag
local strTag = tblLst[strName]
strTag = strTag:replace(".NOTE",".NOTE2")
strTag = strTag:replace("~OBJE","~OBJE2")
tblTgl[strName] = ( strName == strTag )
for _, strTarget in ipairs ( tblRelated[strName] or {} ) do
tblTgl[strTarget] = ( strTarget == strTag )
end
tblTgl[strTag] = true
end
end
if tblTgl.Text then -- Plain Text mode -- 1 Jan 2014
tblTgl.Text = " Plain Text "
if tblVal.Seek:match("^["..StrSP.."]+$") then -- V2.1 Exclude seek for white space &/or punctuation from Whole Word search
tblTgl.Word = false
end
if tblTgl.Word -- Whole Word mode -- 1 Jan 2014
and tblVal.Seek ~= "" then -- Except empty search string -- 5 Jul 2014 V2.3
local strDelimit = "(["..StrSP.."\02])" -- Replace whole words delimited by white space or punctuation or STX="\02" characters
tblVal.Seek = tblVal.Seek:gsub("(["..StrSP.."])","%1\02"):plain()
tblVal.Swap = "%1"..tblVal.Swap:inert().."%2"
tblVal.Seek = strDelimit..tblVal.Seek..strDelimit -- Add word delimiters
else
tblVal.Seek = tblVal.Seek:plain() -- Hide any LUA pattern magic characters
tblVal.Swap = tblVal.Swap:inert()
end
if tblTgl.Case then -- Case Insensitive mode
tblVal.Seek = tblVal.Seek:caseless()
end
else
tblTgl.Text = " LUA Pattern " -- LUA Pattern search mode
tblTgl.Case = false -- Must disable Case Insensitive and Whole Word searches
tblTgl.Word = false
end
if tblVal.Seek == "" then
tblVal.Seek = "^$" -- Empty search string must use empty LUA pattern -- V2.0
tblTgl.Word = false -- and disable Whole Word search mode
end
if tblVal.Swap == "" then -- Change to any char < "\07" as it cannot be enterd by user even via Alt+001
tblVal.Swap = "\01" -- Use SOH="\01" character so DoSeekMatch() always detects a change -- V2.0
end
local tblUser = iup_gui.DialogueAttributes("Main") -- Customise the Replace dialogue to use Main dialogue size & position
iup_gui.CustomDialogue("User",tblUser.Raster,tblUser.CoordX,tblUser.CoordY)
DoAllItems() -- Perform the Search and optional Replace process
return true
end -- local function doPerformAction
local function doOutputResultSet(strTitle,strSub,strOldVal,strNewVal) -- Output the Result Set details -- V3.1
if #TblRecord == 0 then -- No matching data, so report and clear Result Set
local strMode = "reported"
if strNewVal then strMode = "replaced" end
iup_gui.MemoDialogue("\n No matching data items have been "..strMode..". \n")
TblResultSet = { Title=strTitle; Sub=strSub; Old=strOldVal; New=strNewVal; }
return false
end
TblResultSet = {}
fhOutputResultSetTitles(strTitle..strSub, strTitle, strSub.." Date: %#x")
fhOutputResultSetColumn("Record Name", "item", TblRecord , #TblRecord, 180, "align_left")
fhOutputResultSetColumn("Data Ref" , "text", TblDataRef , #TblRecord, 150, "align_left")
fhOutputResultSetColumn("Data Ref" , "item", TblRefValue, #TblRecord, 250, "align_left", 0, true, "default", "buddy")
fhOutputResultSetColumn( strOldVal , "text", TblOldValue, #TblRecord, 250, "align_left")
if strNewVal then
fhOutputResultSetColumn( strNewVal , "text", TblNewValue, #TblRecord, 250, "align_left") -- Only for replacement
end
fhOutputResultSetColumn("Ref Value" , "item", TblRefValue, #TblRecord, 250, "align_left", 0, true, "default", "buddy")
return true
end -- local function doOutputResultSet
local function doSearchReplace() -- Search & Replace action
saveOptions()
local strSeek = tblVal.Seek
local strSwap = tblVal.Swap
tblSet.Mode = "Replace"
if doPerformAction() then
if not TblOption.Tgl.WhSp then -- V3.1
strSeek = strShowWhiteSpace(strSeek)
strSwap = strShowWhiteSpace(strSwap)
end
local strTitle = "Search and Replace Results " -- Output Result Set for replacements -- V3.1
local strSubTitle = "Replacing"..tblTgl.Text..strSeek.." with "..strSwap
if doOutputResultSet(strTitle,strSubTitle,"Old Value","New Value") then return iup.CLOSE end
end
end -- local function doSearchReplace
function btnBasicRepl:action() -- Action for Basic Search & Replace button -- V2.1
return doSearchReplace()
end -- function btnBasicRepl:action
function btnExtraRepl:action() -- Action for Extra Search & Replace button
return doSearchReplace()
end -- function btnExtraRepl:action
local function doSearchOnly() -- Search Only action
saveOptions()
local strSeek = tblVal.Seek
tblVal.Swap = "\03" -- Use ETX="\03" character so DoSeekMatch() always detects a change -- 1 Jan 2014
tblSet.Mode = "Report"
if doPerformAction() then
if not TblOption.Tgl.WhSp then -- V3.1
strSeek = strShowWhiteSpace(strSeek)
end
local strTitle = "Search Results " -- Output Result Set for reported text -- V3.1
local strSubTitle = "Searching for"..tblTgl.Text..strSeek
if doOutputResultSet(strTitle,strSubTitle,"Data Value") then return iup.CLOSE end
end
end -- local function doSearchOnly
function btnBasicSeek:action() -- Action for Basic Search ONLY button -- V2.1
return doSearchOnly()
end -- function btnBasicSeek:action
function btnExtraSeek:action() -- Action for Extra Search ONLY button
return doSearchOnly()
end -- function btnExtraSeek:action
function lstSetPreset:action(strCurr,intCurr,intState) -- Action for Set Preset droplist -- V2.5
tblSet.Curr = intCurr
tglSetPreset.Value = tblSet.List[intCurr].Lock or "OFF"
if intState == 0 then
saveOptions() -- Save current settings in old Preset database -- V2.5
else
if not TblOption[intCurr].Val then
UserDefaultOptions(TblOption[intCurr]) -- Create default database for undefined Preset -- V2.5
end
setControls() -- Load new Preset database into current settings -- V2.5
end
setToolTip(lstSetPreset) -- Refresh XP Tooltip -- V2.3
end -- function lstSetPreset
function tglSetPreset:action(intState) -- Action for Lock preset toggle -- V2.5
saveOptions()
setControls()
setToolTip(tglSetPreset) -- Refresh XP Tooltip -- V2.3
end -- function tglSetPreset:action
function btnSetPreset:action() -- Action for Manage Presets button -- V2.5
saveOptions()
ManagePresets()
lstSetPreset[#tblSet.List+1] = iup.NULL -- Curtail shortened Preset dropdown list
setControls() -- Load new Preset database into current settings -- V2.5
end -- function btnSetPreset:action
function btnDefault:action() -- Action for Restore Defaults button
ResetDefaultSettings()
setControls() -- Reset controls & redisplay Main dialogue
iup_gui.ShowDialogue("Main")
SaveSettings() -- Save sticky data settings
end -- function btnDefault:action
function btnSetFont:action() -- Action for Set Window Font button -- V2.1
btnSetFont.Active = "No"
saveOptions()
iup_gui.FontDialogue(tblControls)
SaveSettings() -- Save sticky data settings
btnSetFont.Active = "Yes"
end -- function btnFontSet: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/search-and-replace"
local arrHelp = { "-major-options-tab"; "-extra-filters-tab"; }
function btnGetHelp:action() -- Action for Help & Advice button according to current tab
local strPage = arrHelp[IntTabPosn] or ""
doExecute( strHelp..strPage )
fhSleep(3000,500)
dialogMain.BringFront="YES"
end -- function btnGetHelp:action
function doDestroy() -- Close Plugin actions -- V3.1
saveOptions()
if TblResultSet.Title then -- Clear the Result Set
TblRecord[1] = fhNewItemPtr()
TblDataRef[1] = ""
TblOldValue[1] = "No matching data items"
TblNewValue[1] = ""
TblRefValue[1] = fhNewItemPtr()
doOutputResultSet(TblResultSet.Title,TblResultSet.Sub,TblResultSet.Old,TblResultSet.New)
end
end -- function doDestroy
--[=[
function btnDestroy:action() -- Action for Close Plugin button -- V3.1
doDestroy()
return iup.CLOSE
end -- function btnDestroy:action
--]=]
function dialogMain:close_cb() -- Close X button callback action -- V3.1
doDestroy()
end -- function dialogMain:close_cb
function tabControl:tabchangepos_cb(intNew,intOld) -- Call back when Main tab position is changed
IntTabPosn = intNew + 1
setTabMode() -- Set toggles for current tab
setWarnMode()
-- saveOptions() -- Could save IntTabPosn sticky setting ?
end -- function tabControl:tabchangepos_cb
iup_gui.ShowDialogue("Main",dialogMain,btnDestroy)
end -- function GUI_MainDialogue
-- Main Code Section Starts Here --
fhInitialise(5,0,8,"save_recommended") -- 5.0.8 for Project/User/Machine Plugin Data needed for Presets
PresetGlobalData() -- Preset global data definitions
ResetDefaultSettings() -- Preset default sticky settings
LoadSettings() -- Load sticky data settings
iup_gui.CheckVersionInStore() -- Notify if later Version
GUI_MainDialogue() -- Invoke graphical user interface
--[[
@Title: Search and Replace
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 4.1
@Keywords:
@LastUpdated: 05 Mar 2025
@Licence: This plugin is copyright (c) 2025 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: Searches for and replaces any given text or date within the current project, with an option to ask for confirmation for each change.
At the end, lists all changed fields with links back to the record (double-click on an item to view it in the Property Box).
@TBD: Check replacement success/failure for duplicate Place names, etc; New option to support external files;
@V4.1: Make StrFileWarning() conditional on Confirm option; Add Source Template ~FDEF.CODE & ~FDEF.TYPE tags;
@V4.0: Revised how Multimedia Linked File fields and Media files filter operates; See StrFileWarning() and doWarning() and doMediaWarning();
@V3.9: Allow Unicode filepaths when changing File names;
@V3.8: Correct 'Case Sensitive' mispelling; Fix broken templated source metafields; Add extra templated source Place & Address options; Add Place and Address field options;
@V3.7: Fix problem with < > in plain text fields;
@V3.6: Fix odd custom fact names like 'An (ODD) Fact (INDI.EVEN-AN_ODD_FACT)'; Update to Library V3.3;
@V3.5: Add Standardized _PLAC.STAN to Record Names/Titles; Updated some other field names;
@V3.4: Add collectgarbage() memory management; Fix for Data Class of Metafields;
@V3.3: Correct updating of Rich Text involving object links;
@V3.2: Replace Place records first to avoid new records in DoAllItems(); Preserve rich text record links in doManageString(...); Minor updates to Major Options tab dialogue;
@V3.1: Updated library to Functions Prototypes v3.0; iup_gui.Balloon = "NO" for PlayOnLinux/Mac; Show/Hide white space in Result Set; Clear Result Set if no data; Prompt GUI taller wordwrap boxes; FH V7 Lua 3.5 IUP 3.28;
@V3.0: Libray V2.9, Show/Hide white space chars as in Word, rearrange Search Scope, Basic Filters, Search Criteria on Major Options tab, add LMO/Sort Date (~OBJE._DATE).
@V2.9: When Media Linked File is Replaced then similarly alter Media file path itself, include FILE tag previously omitted, and cater for both OBJE.FORM and PLAC.FORM tags.
@V2.8: Fix StrSP where [ needs no % escape, new Select Records Search Scope filter, and updated library modules.
@V2.7: Extra format checks and completion report for Import Preset Files, and other minor improvements.
@V2.6: Initialisation progress bars for large Projects, add multi-selection and Import/Export Presets to Manage Presets dialogue.
@V2.5: Support named local Project Presets and global Program Presets, revised tblFilters titles & tooltips, and change for strDataClass == "date" in DoSeekMatch().
@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 uses NaturalSize,
@V2.4: iup_gui update, fix test for newline in Search/Replace, add Place records & Witness Role fields, update ToolTips for ShortText, replace CP1252 … with ASCII ~ to allow both ANSI & UTF-8 encoding.
@V2.3: Date Phrase Warnings inactive when Date unticked, add BalloonToggle().
@V2.2: iup_gui update to fix positioning of Confirmation popup after Restore Defaults, fix for interpreted Date Phrases, toggle for Date Phrase Warnings, and other minor fixes.
@V2.1: iup_gui update, comments removed, Attribute values & other dropdown lists, support tab & newline, inhibit Whole Words for spaces/punctuation, allow repeated words, fix XP tooltips, etc.
@V2.0: Better Item Details, Attribute values, more field search options, Search Only mode, updated GUI, Help & Advice, Sticky Settings, Libraries, etc.
@V1.7: Add the Date and specific Tag related field search & replace capability (see lines marked with -- V1.7) plus major code tidy.
@V1.6: Add more details to the replace prompt for the context of the field being changed.
@V1.5: General code tidy (not published).
@V1.4: Add options for whole word search only and prevent wild card patterns being used automatically
@V1.3: Add Case insensitive searching and cancel or close the window will abort the replaces, while still listing the changes made.
@V1.2: Change to Provide a Skip Button, to cancel click the close button on the window.
@V1.1: Add Skip tick box to replace prompt.
--]]
if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end
--[[
@Title: aa Library Functions Preamble
@Author: Mike Tate
@Version: 3.7
@LastUpdated: 12 Dec 2024
@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.0
@LastUpdated: 27 Aug 2020
@Description: Progress Bar library module.
@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.CENTER -- Show window default position is central
local intPosY = iup.CENTER
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
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.2
@LastUpdated: 07 Oct 2024
@Description: Graphical User Interface Library Module
@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, Help, 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 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","Help","Main" by default
local arg = {...}
local tblNames = {"Font";"Help";"Main";}
for intName, strName in ipairs(arg) do
if type(strName) == "string"
and strName ~= "Font"
and strName ~= "Help"
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.CENTER
tblName.CoordY = iup.CENTER
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(0,intX,intMaxW-intWide) -- Ensure X co-ordinate positions window on screen
end
if intY and intY < iup.CENTERPARENT then
intY = intDimension(0,intY,intMaxH-intHigh) -- Ensure Y co-ordinate positions window on screen
end
tblName.Raster = strRas or "x"
tblName.CoordX = tonumber(intX) or iup.CENTER
tblName.CoordY = tonumber(intY) or iup.CENTER
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","Help","Main" by default
for intName, strName in ipairs(tblOfNames(...)) do
local tblName = tblNameFor(strName)
--# tblName.Raster = tostring(fh.LoadLocal(strName.."S",tblName.Raster)) -- Legacy of "S" becomes "R"
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.FontSet = tonumber(fh.LoadGlobal("Fonts" ,fh.FontSet)) -- Legacy only
fh.FontSet = tonumber(fh.LoadGlobal("FontSet" ,fh.FontSet)) -- Legacy only
fh.History = tostring(fh.LoadGlobal("History" ,fh.History))
fh.Balloon = tostring(fh.LoadGlobal("Balloon" ,fh.Balloon, "Machine"))
fh.LoadDialogue(...)
if fh.FontSet > 0 then fh.FontAssignment(fh.FontSet) end -- Legacy only
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
--# doSaveData(fh.ComputerName.."-"..strParam,strFolder,strScope) -- Uses relative paths to let Paths change
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","Help","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
if tonumber(tblName.CoordX) == nil
or tonumber(tblName.CoordX) < 0 -- V3.5
or tonumber(tblName.CoordX) > intMaxW then
tblName.CoordX = iup.CENTER
end
if tonumber(tblName.CoordY) == nil
or tonumber(tblName.CoordY) < 0 -- V3.5
or tonumber(tblName.CoordY) > intMaxH then
tblName.CoordY = iup.CENTER
end
end -- function CheckWindowPosition
function fh.IsNormalWindow(iupDialog) -- Check dialogue window is not Maximised or Minimised (now redundant)
-- tblPosn[1] = origin x, tblPosn[2] = origin y, tblPosn[3] = width, tblPosn[4] = height
local tblPosn = stringx.splitnumbers(iupDialog.ScreenPosition)
local intPosX = tblPosn[1]
local intPosY = tblPosn[2]
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
return true
end -- function IsNormalWindow
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]
local intPosY = tblPosn[2]
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
iupDialog.MaxSize = intMaxW.."x"..intMaxH -- Maximum size is screen size
iupDialog.MinSize = "x" -- Minimum size (default "x" becomes nil)
iupDialog.RasterSize = tblName.Raster or "x" -- Raster size (default "x" becomes nil)
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) fh.SetWindowCoord(tblName) end -- V3.5
iupDialog.resize_cb = iupDialog.resize_cb or function(self) if fh.SetWindowCoord(tblName) then tblName.Raster=self.RasterSize end end -- V3.5
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
fh.RefreshDialogue(strName) -- Refresh to set Natural Size as Minimum Size
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.CENTER
tblName.CoordY = nil -- iup.CENTER
else
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 ")
---[=[
local intFontPlain = 1 -- Font Face & Style values for legacy FontSet setting
local intFontBold = 2
local intArialPlain = 3
local intArialBold = 4
local intTahomaPlain= 5
local intTahomaBold = 6
local strFontFace = fh.FontBody:gsub(",.*","")
local tblFontSet = {} -- Lookup table for FontHead and FontBody
tblFontSet[intFontPlain] = { Head=strFontFace.."; Bold -16"; Body=strFontFace.."; -15"; }
tblFontSet[intFontBold] = { Head=strFontFace.."; Bold -16"; Body=strFontFace.."; Bold -15"; }
tblFontSet[intArialPlain] = { Head="Arial; Bold -16"; Body="Arial; -16"; }
tblFontSet[intArialBold] = { Head="Arial; Bold -16"; Body="Arial; Bold -15"; }
tblFontSet[intTahomaPlain] = { Head="Tahoma; Bold -15"; Body="Tahoma; -16"; }
tblFontSet[intTahomaBold] = { Head="Tahoma; Bold -15"; Body="Tahoma; Bold -14"; }
function fh.FontAssignment(intFontSet) -- Assign Font Face & Style GUI values for legacy FontSet setting
if intFontSet then
intFontSet = math.max(intFontSet,1)
intFontSet = math.min(intFontSet,#tblFontSet)
fh.FontHead = tblFontSet[intFontSet]["Head"] -- Legacy Font for all GUI dialog header text
fh.FontBody = tblFontSet[intFontSet]["Body"] -- Legacy Font for all GUI dialog body text
end
end -- function FontAssignment
--]=]
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 and Help dialogues
local tblHelp = tblNameFor("Help")
if type(tblHelp.Dialog) == "userdata" then -- Help dialogue exists
fh.AssignAttributes(tblHelp.TblAttr) -- Assign the Help dialogue attributes
fh.RefreshDialogue("Help") -- Refresh the Help window size & position
end
fh.AssignAttributes(tblAttr) -- Assign parent dialogue attributes
fh.RefreshDialogue(strName) -- Refresh parent window size & position and bring infront of Help 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 end -- Return label control so message can be changed
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 -- V3.9
local strVersion = "0"
if strPlugin then
local strFile = fh.MachinePath.."\\VersionInStore "..strPlugin..".dat"
local intTime = os.time() - 2600000 -- Time in seconds a month ago -- V3.9
local tblAttr, strError = lfs.attributes(strFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago -- V3.9
local strErrFile = fh.MachinePath.."\\VersionInStoreInternetError.dat"
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..strPlugin
local isOK, strReturn = pcall(httpRequest,strRequest)
if not isOK then -- Problem with Internet access
local intTime = os.time() - 36000 -- Time in seconds 10 hours ago
local tblAttr, strError = lfs.attributes(strErrFile) -- Obtain file attributes
if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ","MB_OK","MB_ICONEXCLAMATION")
end
general.SaveStringToFile(strErrFile,strErrFile) -- Update file modified time
else
general.DeleteFile(strErrFile) -- Delete file if Internet is OK
if strReturn ~= nil then
strVersion = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
general.SaveStringToFile(strVersion,strFile) -- Update file modified time and save version -- V4.1
end
end
else
strVersion = general.StrLoadFromFile(strFile) -- Retrieve saved latest version -- V4.1
if #strVersion > 9 then general.DeleteFile(strFile) end
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
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 Family Historian '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.CustomDialogue("Help","1020x730") -- Custom "Help" dialogue sizes
fh.DefaultDialogue() -- Default "Font","Help","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()
iup_gui.Gap = "2"
iup_gui.Balloon = "NO" -- Needed for PlayOnLinux/Mac -- V3.1
iup_gui.SetUtf8Mode()
StrSP = "\t-\r !-/:-@[-`{-~" -- Substitute LUA pattern for %s%p ( [ is not magic here so needs no % escape )
StrMid_Dot = "·" -- Space symbol
StrRtArrow = "»" -- Hor Tab symbol
StrPilcrow = "¶" -- Newline symbol
if fhGetAppVersion() > 5 then
StrMid_Dot = fhConvertANSItoUTF8(StrMid_Dot) -- White space UTF8 symbols -- V3.0
StrRtArrow = fhConvertANSItoUTF8(StrRtArrow)
StrPilcrow = fhConvertANSItoUTF8(StrPilcrow)
end
DicEncode = { } -- White space encoding -- V3.0
DicEncode[" "] = StrMid_Dot
DicEncode["\t"] = StrRtArrow.."\t"
DicEncode["\n"] = StrPilcrow.."\n"
IntCaret = -9 -- Text edit caret position -- V3.0
ArrOption = {} -- Array of global Program Preset GUI options -- V2.5
TblOption = {} -- Table of local Project Preset GUI options
TblRecord = {} -- Result Set tables
TblDataRef = {}
TblOldValue = {}
TblNewValue = {}
TblRefValue = {} -- V3.1
TblResultSet= { Title="Search and Replace "; Sub="No Matching Data"; Old="Data Value"; }
end -- function PresetGlobalData
-- Set User Default Options --
function UserDefaultOptions(tblOption)
if not tblOption.Val then tblOption.Val = {} end -- Search and Replace strings are in Val sub-table
if not tblOption.Lst then tblOption.Lst = {} end -- All dropdown list settings are in Lst sub-table
if not tblOption.Tgl then tblOption.Tgl = {} end -- All toggle ON/OFF settings are in Tgl sub-table
tblOption.Val.Seek = "" -- Search text string empty
tblOption.Val.Swap = "" -- Replace text string empty
for strName, anyValue in pairs (TblOption.Lst) do
tblOption.Lst[strName] = "" -- Reset all GUI dropdown lists associated with toggles
end
for strName, anyValue in pairs (TblOption.Tgl) do
tblOption.Tgl[strName] = "ON" -- Enable all GUI toggle options except as below
end
tblOption.Tgl.Conf = "ON" -- V2.1 Ensure Confirmation toggle is ticked
tblOption.Tgl["SOUR.TITL"]= "ON" -- "OFF" -- V2.1 Parent Child Tag for Source Titles -- V1.7
tblOption.Tgl.age = "ON" -- "OFF" -- V2.1 Class age is just the Age fields -- V1.7
tblOption.Tgl.Age = "OFF" -- V2.1 Age field error checks
tblOption.Tgl.date = "OFF" -- V1.7 Class date is all the Date fields
tblOption.Tgl.Date = "OFF" -- V1.7 Date field warnings
tblOption.Tgl.Phrase = "ON" -- V2.2 Date Phrase warnings
end -- function UserDefaultOptions
-- 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("Preset","0x0") -- Custom "Preset" dialogue minimum size & centralisation
iup_gui.DefaultDialogue("Preset","Bars","Memo") -- GUI window rastersize and X & Y co-ordinates for "Main","Font","Preset","Bars","Memo" dialogues
IntTabPosn = 0 -- Tab position undefined
UserDefaultOptions(TblOption) -- GUI Modes are mixed case, Classes are lower case, Tags are upper case
if not TblOption.Set then TblOption.Set = {} end -- Non-toggle option settings are in Set sub-table
TblOption.Set.Item = 0 -- Item count of database size
TblOption.Set.Date = "" -- Date of last Item update
TblOption.Set.Recs = nil -- Record Ids Search Scope filter off -- V2.8
local intCurr = tonumber(TblOption.Set.Curr) or 1
TblOption[intCurr] = {} -- Clear current Project Preset settings -- V2.5
if TblOption.Set.List then
TblOption.Set.List[intCurr].Lock = "OFF"
end
IsMemoFail = true -- V3.4
end -- function ResetDefaultSettings
-- Load Sticky Settings from File --
function LoadSettings()
iup_gui.LoadSettings("Preset","Bars") -- Includes "Main","Font" dialogues and "FontSet" & "History"
ArrOption = iup_gui.LoadGlobal("Option",ArrOption,"Machine") -- V2.5
TblOption = iup_gui.LoadGlobal("Option",TblOption)
if not ArrOption.Set then ArrOption.Set = {} end
if not ArrOption.Set.List then ArrOption.Set.List = {} end -- V2.5
for intList = 1, #ArrOption.Set.List do
if not ArrOption[intList] then ArrOption[intList] = {} end -- V2.5 Ensure each Program Preset entry exists
end
if not TblOption.Set then TblOption.Set = {} end -- Cope with legacy settings
if not TblOption.Val then TblOption.Val = {} end
if not TblOption.Lst then TblOption.Lst = {} end
if TblOption.Set.Lock then TblOption.Set.Lock = nil end -- V2.5
local arrList = {}
if not TblOption.Set.List then
TblOption.Set.List = {}
TblOption.Set.Curr = 1
arrList = { "Alpha"; "Bravo"; "Charlie"; "Delta"; } -- Table of local Project Preset names -- V2.5
elseif type(TblOption.Set.List[1]) == "string" then
arrList = TblOption.Set.List -- Legacy local Project Preset list
end
for intList, strName in ipairs (arrList) do
TblOption.Set.List[intList] = { Name=strName; Lock="OFF"; }
end
for intList = 1, #TblOption.Set.List do
if not TblOption[intList] then TblOption[intList] = {} end -- V2.5 Ensure each Project Preset entry exists
if not TblOption.Set.List[intList].Lock then TblOption.Set.List[intList].Lock = "OFF" end
end
TblOption.Set.Curr = math.min(tonumber(TblOption.Set.Curr) or 1,#TblOption.Set.List)
TblOption.Val.Seek = TblOption.Set.Seek -- V2.5 Move legacy Search & Replace strings
TblOption.Val.Swap = TblOption.Set.Swap
TblOption.Set.Seek = nil
TblOption.Set.Swap = nil
TblOption.Tgl["SOUR.TITL"]= TblOption.Tgl["SOUR.TITL"] or "ON" -- V1.7 Parent Child Tag for Source Titles
TblOption.Tgl.Conf = TblOption.Tgl.Conf or "ON" -- V2.1 Ensure Confirmation toggle is ticked
TblOption.Tgl.age = TblOption.Tgl.age or "ON" -- V1.7 Class age is just the Age fields
TblOption.Tgl.Age = TblOption.Tgl.Age or "OFF" -- V2.1 Age field error checks
TblOption.Tgl.date = TblOption.Tgl.date or "OFF" -- V1.7 Class date is all the Date fields
TblOption.Tgl.Date = TblOption.Tgl.Date or "OFF" -- V1.7 Date field warnings
TblOption.Tgl.Phrase = TblOption.Tgl.Phrase or "ON" -- V2.2 Date Phrase warnings
SaveSettings() -- Save sticky data settings
end -- function LoadSettings
-- Save Sticky Settings to File --
function SaveSettings()
local datToday = fhNewDate(2000)
datToday:SetSimpleDate(fhCallBuiltInFunction("Today")) -- Obtain date today
local strToday = datToday:GetDisplayText("ABBREV")
if TblOption.Set.Date ~= strToday or not TblOption.Set.INDI then -- Different date or no record type counts -- V2.3
TblOption.Set.Date = strToday -- So count database items for progress bar -- V2.3
progbar.Setup()
progbar.Start("Initialisation",fhGetRecordTypeCount()) -- Initialisation Progress Bar -- V2.6
local intItem = 0
for strType in iterate.RecordTypes() do -- Loop through all record types -- V2.3
progbar.Message("Counting "..strType.." Record Items ~ Please Wait")
progbar.Step() -- Step progress bar
local intType = 0
for ptrItem in iterate.Items(strType) do -- Loop through all record type items
intType = intType + 1 -- Count number of record type items -- V2.3
intItem = intItem + 1 -- Count number of all database items
if ( intItem % 8384 ) == 0 and progbar.Stop() then error("\n Plugin Aborted. ") end
end
TblOption.Set[strType] = intType -- Save count of record type items -- V2.3
end
TblOption.Set.Item = intItem -- Save count of all database items -- V2.3
progbar.Close()
end
iup_gui.SaveGlobal("Option",TblOption)
iup_gui.SaveGlobal("Option",ArrOption,"Machine") -- V2.5
iup_gui.SaveSettings("Preset","Bars") -- Includes "Main","Font" dialogues and "FontSet" & "History"
end -- function SaveSettings
function strHideWhiteSpace(strText,strChar) -- Remove special white space symbols, or replace with \01 byte -- V3.0
strChar = strChar or ""
strText = strText:gsub(StrMid_Dot," "):gsub(StrRtArrow,strChar):gsub(StrPilcrow,strChar)
return strText
end -- function strHideWhiteSpace
function strShowWhiteSpace(strText) -- Insert special white space symbols, after removing \01 byte -- V3.0
strText = strText:gsub("\01","")
strText = strText:gsub("([ \t\n])",DicEncode)
return strText
end -- function strShowWhiteSpace
function doChange(iupSelf) -- Handle text edits when showing white space symbols -- V3.0
local intPosn = tonumber(iupSelf.CaretPos)
if not TblOption.Tgl.WhSp
or TblOption.Tgl.WhSp == "OFF" then
local strText = strHideWhiteSpace(iupSelf.Value,"\01") -- Replace » and ¶ multi-byte symbols with \01 byte
local strPrev = strText:sub(intPosn-1,intPosn-1)
local strCurr = strText:sub(intPosn,intPosn)
local strNext = strText:sub(intPosn+1,intPosn+1)
if strCurr == "\01" or strNext == "\t" or strNext == "\n" then -- Deletion of Tab or Newline with Backspace or Delete key
if strCurr ~= "\01" then intPosn = intPosn + 1 end -- Adjust caret position when Delete key deletes » or ¶ symbol
strText = strText:sub(1,intPosn-1)..strText:sub(intPosn+1) -- Remove either \01 byte or Tab/Newline character
intPosn = intPosn - 1 -- Adjust caret position for deleted character
end
if strPrev ~= "\01" and (strCurr == "\t" or strCurr == "\n") then -- Insertion of Tab or Newline
intPosn = intPosn + 1 -- Adjust caret position for inserted » or ¶ symbol
end
iupSelf.Value = strShowWhiteSpace(strText)
iupSelf.CaretPos = intPosn
end
IntCaret = intPosn
end -- function doChange
function doCaret(iupSelf,intPosn) -- Handle cursor moves when showing white space symbols -- V3.0
if not TblOption.Tgl.WhSp
or TblOption.Tgl.WhSp == "OFF" then
local strText = strHideWhiteSpace(iupSelf.Value,"\01") -- Replace » and ¶ multi-byte symbols with \01 byte
if strText:sub(intPosn,intPosn) == "\01" then -- Skip over special » and ¶ symbols
if intPosn > IntCaret then
intPosn = intPosn + 1 -- Moving left to right
else
intPosn = intPosn - 1 -- Moving right to left
end
iupSelf.CaretPos = intPosn
end
end
IntCaret = intPosn
end -- function doCaret
function StrAgeWarning(ptrItem,strNewAge) -- Returns warning text or nil if OK, plus the new Age text
-- ptrItem ~ Pointer to age item
-- strNewAge ~ New value for age item
if #strNewAge > 0 and TblOption.Set.Mode == "Replace" then -- No age error checks unless Age Item exists in Replace mode
local strOldAge = fhGetValueAsText(ptrItem)
if TblOption.Tgl.Age then -- Age field warnings enabled?
fhSetValueAsText(ptrItem,strNewAge) -- Check for invalid Age returning blank
local strAgeText = fhGetValueAsText(ptrItem)
fhSetValueAsText(ptrItem,strOldAge) -- Restore the original Age value
if strAgeText == "" then
return " ! ! BEWARE ! ! Replacement age is invalid. \n The field will be DELETED if 'Replace' is confirmed. ",strNewAge
else
return nil,strAgeText -- Otherwise, return nil warning with corrected Age text
end
end
end
return nil,strNewAge -- For OK values or no warning return nil, and original text
end -- function StrAgeWarning
function StrDateWarning(ptrItem,strNewVal) -- Returns warning text or nil if date is OK
-- ptrItem ~ Pointer to date item
-- strNewVal ~ New value for date item
if #strNewVal > 0 and TblOption.Set.Mode == "Replace" then -- No date warnings unless Date Item exists in Replace mode
local oldDate = fhGetValueAsDate(ptrItem)
local newDate = fhNewDate(1999) -- For "date" value type use Date Object
local strIDP = " %(\".*\"%)$" -- LUA pattern for Interpreted 'Date ("Phrase")' -- V2.2
if newDate:SetValueAsText(strNewVal:gsub(strIDP,""),false) -- Is new Date valid, allowing for Interpreted Date Phrase -- V2.2
and newDate:SetValueAsText(strNewVal,true) then
if TblOption.Tgl.Date then -- Date field warnings enabled?
fhSetValueAsDate(ptrItem,newDate) -- Check for invalid Date warnings
local strWarning = fhCallBuiltInFunction("GetDataWarning",ptrItem,1)
fhSetValueAsDate(ptrItem,oldDate) -- Restore the original Date value
if strWarning ~= "" then
return " ! ! BEWARE ! ! Replacement date is inconsistent. \n "..strWarning.." \n It will be set anyway if 'Replace' is confirmed. "
end
end
elseif TblOption.Tgl.Phrase then -- Date Phrase warnings enabled? -- V2.2
if strNewVal:match(strIDP) then -- Cater for Interpreted Date Phrase and plain Date Phrase -- V2.2
return " ! ! BEWARE ! ! Replacement interpreted date is invalid. \n It will become part of the Date Phrase if 'Replace' is confirmed. "
else
return " ! ! BEWARE ! ! Replacement date is invalid. \n It will be set as a Date Phrase if 'Replace' is confirmed. "
end
end
end
return nil -- For OK values or no warning return nil
end -- function StrDateWarning
function StrFileWarning(ptrItem,strNewVal,strOldVal) -- Returns warning text for Media file issues or nil otherwise -- V4.0
-- ptrItem ~ Pointer to data item
-- strNewVal ~ New value for data item
-- strOldVal ~ Old value for data item
local strWarn = nil
if fhGetTag(ptrItem):match("FILE") and TblOption.Set.Mode == "Replace" then -- Updating linked Media file path -- V4.0
local strPath = general.SplitFilename(fhGetContextInfo("CI_GEDCOM_FILE"))
if not general.FlgFileExists(StrMediaRelToAbs(strPath,strNewVal)) then
if general.FlgFileExists(StrMediaRelToAbs(strPath,strOldVal)) then
strWarn = " ! ! BEWARE ! ! See the Help && Advice for details. \n\n"
local _,_,strOldExt = general.SplitFilename(strOldVal) -- Replacement Value file does not exist and Current Value file does exist
local _,_,strNewExt = general.SplitFilename(strNewVal)
strOldExt = strOldExt:upper()
strNewExt = strNewExt:upper()
if strOldExt == strNewExt then
if TblOption.Tgl.Conf then -- Warning conditional on confirmation setting -- V4.1
strWarn = strWarn .. " The Current Value file will be copied to the Replacement Value file if 'Replace' is confirmed. "
else
strWarn = nil
end
else
strWarn = strWarn .. " This change is not allowed as it involves altering the file format from "..strOldExt.." to "..strNewExt..". "
end
end
end
end
return strWarn
end -- function StrFileWarning
function GetDataClass(ptrItem,strChild) -- Get data class & child tag for all items including metafields -- V3.4 -- V3.8
-- ptrItem ~ Pointer to data item
-- strChild ~ Current child tag
local tblMeta = {
TX = { Child="TEXT" ; Class="text" ; } ;
NM = { Child="NAME" ; Class="name" ; } ;
DT = { Child="DATE" ; Class="date" ; } ;
PL = { Child="PLAC" ; Class="place" ; } ;
AD = { Child="ADDR" ; Class="longtext" ; } ;
RP = { Child="LINK" ; Class="link" ; } ;
EN = { Child="ENUM" ; Class="enumword" ; } ;
UL = { Child="URL" ; Class="url" ; } ;
}
local strClass = fhGetDataClass(ptrItem) -- Get basic item data class
if strClass == "metafield" then
local dicMeta = tblMeta[ fhGetMetafieldType(ptrItem) ]
strChild = "~_FIELD."..dicMeta.Child -- Add metafield child tag
strClass = dicMeta.Class -- Get metafield data class
end
return strClass,strChild
end -- function GetDataClass
-- Search & Replace Prompt GUI --
function SearchReplacePrompt(ptrItem,strOldVal,strNewVal,strWarning)
-- ptrItem ~ Pointer to data item
-- strOldVal ~ Old value of data item
-- strNewVal ~ New value for data item
-- strWarning~ Warning message for Age/Date value
local function intLineCount(strText,maxLines)
--[[
@Function: intLineCount
@Description:Works out approximate number of lines to display a text string in a GUI text box.
@Parameters:
strText ~ String to process mandatory
maxLines ~ Maximum height for text box, defaults to 9 lines if not provided
]]
maxLines = maxLines or 9
local intLines = 1
for strLine in string.gmatch(strText.."\n","(.-)\n") do -- Count lines and increase for long wrapped lines -- V3.1
intLines = intLines + math.ceil( #strLine / 150 )
end
return math.min(intLines,maxLines)
end -- local function intLineCount
local function strItemDetails(ptrItem) -- Returns Record Tag and [Id] plus full Data Ref, and descriptive text for each Tag level
-- ptrItem ~ Pointer to data item
local tblDetails = {}
local ptrTemp = ptrItem:Clone()
local strDataRef, intRecId, strRecTag = general.BuildDataRef(ptrItem)
TblOption.Set.Data = strDataRef -- Save the Data Ref for Result Set
table.insert(tblDetails,strRecTag.." Record Id: ["..intRecId.."] Data Ref: "..strDataRef)
while ptrTemp:IsNotNull() do
local strUDF = ""
if fhIsUDF(ptrTemp) then -- V2.0 adds UDF indication
strWarning = " ! ! BEWARE ! ! Unrecognised Data Field (UDF)"
strUDF = " {*UDF!}"
end
table.insert(tblDetails,2,fhGetDisplayText(ptrTemp)..strUDF)
ptrTemp:MoveToParentItem(ptrTemp)
end
return table.concat(tblDetails,"\n")
end -- local function strItemDetails
local function iupMultiLineText(strVal,strRead) -- Return iup.text multiline control
-- strVal ~ Text value
-- strRead ~ "Yes" or "No"
return iup.text { Value=strVal, ReadOnly=strRead, VisibleLines=intLineCount(strVal,6), -- Add WordWrap & taller boxes for long text -- V3.1
MultiLine="Yes"; WordWrap="Yes"; AutoHide="Yes"; Formatting="Yes"; AddFormatTag=iup.user { Bulk="Yes"; CleanOut="Yes"; }; }
end -- local function iupMultiLineText
local strButton = "Cancel" -- Default to value for X Close window button
local function iupButtonControl(strTitle) -- Return iup.button control
return iup.button { Title=strTitle; MinSize="100x40"; action=function(self) strButton=self.Title return iup.CLOSE end; }
end -- local function iupButtonControl
if not TblOption.Tgl.WhSp then -- V3.0
strOldVal = strShowWhiteSpace(strOldVal)
strNewVal = strShowWhiteSpace(strNewVal)
end
local strMode = TblOption.Set.Mode -- Mode is either "Replace" or "Report" text
local lblDataRef = iup.label { Title="Data Reference : "; }
local txtDataRef = iupMultiLineText( strItemDetails(ptrItem), "Yes" )
local lblCurrent = iup.label { Title="Current Value : "; }
local txtCurrent = iupMultiLineText( strOldVal, "Yes" )
local lblReplace = iup.label { Title="Replacement Value : "; }
local txtReplace = iupMultiLineText( strNewVal, "No" )
local lblWarning = iup.label { Title=strWarning; }
local tglConfirm = iup.toggle { Title="Confirm every item found"; }
local btnPerform = iupButtonControl( strMode )
local btnSkipOne = iupButtonControl( "Skip" )
local btnDestroy = iupButtonControl( "Cancel" )
local function doWarning() -- Update Warning label and Replace button -- V4.0
btnPerform.Active = "Yes"
lblWarning.Title = strWarning
if strWarning then
lblWarning.Active = "Yes"
if strWarning:match(" not allowed ") then -- Disable the Replace button -- V4.0
btnPerform.Active = "No"
end
else
lblWarning.Active = "No"
end
end -- local function doWarning
doWarning() -- V4.0
if strMode ~= "Replace" then -- Hide the Replacement Value controls
lblReplace.Visible = "No"
lblReplace.Floating = "Yes"
txtReplace.Visible = "No"
txtReplace.Floating = "Yes"
end
if TblOption.Tgl.Conf then -- Set initial Confirmation toggle value
tglConfirm.Value = "ON"
else
tglConfirm.Value = "OFF"
end
local tblConfirm = { ON = "Confirmation prompt for each item found"; OFF = strMode.." all matching text unconditionally"; }
-- Set other GUI control attributes
for iupName, tblAttr in pairs ( {
-- Control = 1~FgColor ; 2~Font ; 3~Expand ; 4~Tip ; 5~action ;
[lblDataRef] = { iup_gui.Head; iup_gui.FontHead ; "No" ; "Data reference details" ; };
[txtDataRef] = { iup_gui.Body; iup_gui.FontBody ; "Horizontal" ; "Data reference details" ; };
[lblCurrent] = { iup_gui.Head; iup_gui.FontHead ; "No" ; "Current text string" ; };
[txtCurrent] = { iup_gui.Body; iup_gui.FontBody ; "Horizontal" ; "Current text string" ; };
[lblReplace] = { iup_gui.Head; iup_gui.FontHead ; "No" ; "Replacement text string that can be edited" ; };
[txtReplace] = { iup_gui.Safe; iup_gui.FontBody ; "Horizontal" ; "Replacement text string that can be edited" ; };
[lblWarning] = { iup_gui.Risk; iup_gui.FontBody ; "Yes" ; "Data warning message" ; };
[tglConfirm] = { iup_gui.Safe; iup_gui.FontBody ; "Yes" ; tblConfirm[tglConfirm.Value]; function() tglConfirm.Tip = tblConfirm[tglConfirm.Value] end; };
[btnPerform] = { iup_gui.Safe; iup_gui.FontBody ; "Horizontal" ; strMode.." the data item" ; };
[btnSkipOne] = { iup_gui.Safe; iup_gui.FontBody ; "Horizontal" ; "Skip this data item" ; };
[btnDestroy] = { iup_gui.Risk; iup_gui.FontBody ; "Horizontal" ; "Cancel rest of search" ; };
} ) do
iupName.FgColor = tblAttr[1]
iupName.Font = tblAttr[2]
iupName.Expand = tblAttr[3]
iupName.Tip = tblAttr[4]
iupName.TipBalloon = iup_gui.Balloon
if tblAttr[5] then iupName.action = tblAttr[5] end
end
local boxDataRef = iup.vbox { lblDataRef; txtDataRef; }
local boxCurrent = iup.vbox { lblCurrent; txtCurrent; }
local boxReplace = iup.vbox { lblReplace; txtReplace; }
local boxControls = iup.vbox { lblWarning; iup.hbox { tglConfirm; btnPerform; btnSkipOne; btnDestroy; Margin="3x3"; Alignment="ABOTTOM"; }; }
local boxDialogue = iup.vbox { boxDataRef; boxCurrent; boxReplace; boxControls; }
local dlgPerform = iup.dialog { Title=iup_gui.Plugin..iup_gui.Version; Gap=iup_gui.Gap; Margin="3x3"; boxDialogue; }
local strDataClass = GetDataClass(ptrItem) -- Fix for metafields -- V3.4
function txtReplace:action(intChar,strText) -- Replacement Value is being changed
-- intChar ~ Character position
-- strText ~ Text string (not used)
if strDataClass ~= "longtext" and string.char(intChar) == "\n" then
return iup.IGNORE -- Prevent short text items exceeding one line
end
end -- function txtReplace:action
function txtReplace:valuechanged_cb() -- Replacement Value has been changed
if strDataClass == "age" then -- Let spaces be added to allow yrs/mns/dys to be entered
local strNewVal = strHideWhiteSpace(strNewVal) -- V3.0
local strTxtVal = strHideWhiteSpace(txtReplace.Value) -- V3.0
if strNewVal:gsub("%s","") == strTxtVal:gsub("%s","") then return end
strWarning,strNewVal = StrAgeWarning(ptrItem,strTxtVal) -- Regenerate any Age Warnings and corrected Value
if strTxtVal ~= strNewVal then
if not TblOption.Tgl.WhSp then -- V3.0
strNewVal = strShowWhiteSpace(strNewVal)
end
local intCaretPos = txtReplace.CaretPos -- Maintain editing caret position and update Value
txtReplace.Value = strNewVal
txtReplace.CaretPos = intCaretPos
end
elseif strDataClass == "date" then -- Regenerate any Date Warnings
strWarning = StrDateWarning(ptrItem,strHideWhiteSpace(txtReplace.Value)) -- V3.0
elseif strDataClass == "text" then -- Generate any File issue Warning
strWarning = StrFileWarning(ptrItem,strHideWhiteSpace(txtReplace.Value),strHideWhiteSpace(strOldVal)) -- V4.0 -- V4.1
end
doWarning() -- V4.0
doChange(txtReplace) -- V3.0
end -- function txtReplace:valuechanged_cb
function txtReplace:caret_cb(lin,col,pos)
doCaret(txtReplace,pos) -- V3.0
end -- function txtReplace:caret_cb
iup_gui.ShowDialogue("User",dlgPerform,btnPerform,"Normal Keep") -- Popup dialogue that remembers its attributes & refreshes its size
strNewVal = strHideWhiteSpace(txtReplace.Value) -- Retrieve new Replacement text and Confirmation toggle values -- V3.0
TblOption.Tgl.Conf = ( tglConfirm.Value == "ON" )
dlgPerform:destroy()
return strButton, strNewVal
end -- function SearchReplacePrompt
-- Convert Media Relative Path to Absolute Path -- -- V3.9
function StrMediaRelToAbs(strPath,strFile)
-- strPath ~ Media folder path
-- strFile ~ Media file name
strFile = strFile:gsub("^[Mm]edia\\",function() return strPath.."Media\\" end)
return strFile
end -- function StrMediaRelToAbs
-- Perform Text Search and Optional Replace --
function DoSeekMatch(ptrItem,strDataClass)
-- ptrItem ~ Pointer to data item
-- strDataClass ~ Data Class of item
local strOldVal = fhGetValueAsText(ptrItem)
local strNewVal = ""
local strSeek = TblOption.Val.Seek -- V2.5 Val
local strSwap = TblOption.Val.Swap
local isOK = true -- V3.4
local function doManageString(strOldVal,strNewVal) -- V1.7 new function
if TblOption.Set.Mode == "Replace" then
if strDataClass == "richtext" then -- V3.2 -- Cater for richtext
local isRichText = false -- V3.7 -- Detect plain text v rich text
local arrLink = {}
local ptrLink = fhNewItemPtr()
ptrLink:MoveToFirstChildItem(ptrItem) -- Save any record link details -- V3.2
while ptrLink:IsNotNull() do
local strLink = fhGetTag(ptrLink)
if strLink:match("^_LINK_%u$") then
local intLink = fhGetValueAsInteger(fhGetItemPtr(ptrLink,'~._LKID'))
arrLink[intLink] = { Tag=strLink; Ptr=fhGetValueAsLink(ptrLink); }
end
isRichText = true -- Subsidiary _LINK_ or _FMT 1 tag found -- V3.7
ptrLink:MoveNext()
end
if isRichText then
isOK = fhSetValueAsRichText(ptrItem,fhNewRichText(strNewVal))-- Update richtext field -- isOK -- V3.4
else
isOK = fhSetValueAsText(ptrItem,strNewVal) -- Update plain text field -- V3.7
end
for intLink, dicLink in pairs(arrLink) do -- V3.3
ptrLink = fhCreateItem(dicLink.Tag,ptrItem) -- Reconstruct any record link details -- V3.2
if ptrLink:IsNotNull() then
fhSetValueAsLink(ptrLink,dicLink.Ptr)
ptrLink = fhCreateItem('_LKID',ptrLink)
if ptrLink:IsNotNull() then
fhSetValueAsInteger(ptrLink,intLink)
end
end
end
elseif strDataClass == "date" then -- V1.7 -- Cater for Date field
local newDate = fhNewDate(1999)
if strNewVal == "" then
newDate:SetNull() -- Null Date Object for empty string
else
newDate:SetValueAsText(strNewVal,true) -- Allow Date Phrase for Date Object
end
isOK = fhSetValueAsDate(ptrItem,newDate) -- isOK -- V3.4
else
if TblOption.Set.Data:match("FILE$") then -- Update linked Media file path? -- V2.9
local strPath = general.SplitFilename(fhGetContextInfo("CI_GEDCOM_FILE"))
local strOldFile = StrMediaRelToAbs(strPath,strOldVal) -- V3.9
local strNewFile = StrMediaRelToAbs(strPath,strNewVal) -- V3.9
isOK = general.FlgFileExists(strNewFile)
or not general.FlgFileExists(strOldFile)
if not isOK then -- Replacement Value file does not exist and Current Value file does exist -- V4.0
local _,_,strOldExt = general.SplitFilename(strOldVal)
local _,_,strNewExt = general.SplitFilename(strNewVal)
strOldExt = strOldExt:upper()
strNewExt = strNewExt:upper()
if strOldExt == strNewExt then
isOK = general.CopyFile(strOldFile,strNewFile) -- Copy linked Media file and alter absolute to relative path -- V4.0 -- V2.9
strNewVal = strNewVal:replace(strPath.."Media","Media")
end
end
end
if isOK then -- V3.9
isOK = fhSetValueAsText(ptrItem,strNewVal) -- For text values use Text object -- isOK -- V3.4
end
end
fhUpdateDisplay()
end
if not TblOption.Tgl.WhSp then -- V3.1
strOldVal = strShowWhiteSpace(strOldVal)
strNewVal = strShowWhiteSpace(strNewVal)
end
local ptrRecord = fhNewItemPtr() -- Save result set entries
ptrRecord:MoveToRecordItem(ptrItem)
if not isOK then -- Replacement failed -- V3.4
if IsMemoFail then
if 1 == iup_gui.WarnDialogue(" !! Replacement Failed ~ Old Value Retained !! "," The Result Set will list that error in the New Value column instead of the replacement: \n\n "..strNewVal.." \n\n Do you wish to inhibit these warning messages? \n\n","Yes","No") then
IsMemoFail = false
end
end
strNewVal = "!! Replacement Failed ~ Old Value Retained !!"
end
table.insert(TblRecord,ptrRecord:Clone())
table.insert(TblDataRef,TblOption.Set.Data)
table.insert(TblOldValue,strOldVal)
table.insert(TblNewValue,strNewVal)
table.insert(TblRefValue,ptrItem:Clone()) -- V3.1
end -- local function doManageString
if strDataClass == "richtext" then -- V3.1 -- Cater for richtext
strOldVal = fhGetValueAsRichText(ptrItem):GetText()
elseif strDataClass == "date" then -- V1.7 -- New conditional statement for Date field
--# strOldVal = fhGetItemText(ptrItem,fhGetTag(ptrItem)..":LONG") -- V2.5
strOldVal = fhGetValueAsDate(ptrItem):GetDisplayText("LONG") -- V3.8 -- Cope with templated source Date metafield
end
if TblOption.Tgl.Word then -- V2.1 revised to cater for successive repetitions of the seek word -- 1 Jan 2014
strNewVal = ("\02"..strOldVal.."\02"):gsub("(["..StrSP.."])","%1\02")-- Insert leading & trailing STX="\02" so first & last words match, and add STX="\02" to each white space/punctuation character so repeated words match
strNewVal = strNewVal:gsub(strSeek,strSwap):gsub("\02","") -- Replace words and remove all inserted STX="\02" characters
else
strNewVal = strOldVal:gsub(strSeek,strSwap) -- Replace any other cases
end
if strOldVal ~= strNewVal then
strNewVal = strNewVal:gsub("\01","") -- V2.0 Eliminate any SOH="\01" characters added by doPerformAction() to force a change
local strWarning = nil
if strDataClass == "age" then -- Obtain any Warning for Age field, with corrected text value
strWarning,strNewVal = StrAgeWarning(ptrItem,strNewVal)
elseif strDataClass == "date" then -- Obtain any Warning for Date field
strWarning = StrDateWarning(ptrItem,strNewVal)
elseif strDataClass == "text" then -- Obtain any Warning for File field -- V4.0
strWarning = StrFileWarning(ptrItem,strNewVal,strOldVal)
end
if TblOption.Tgl.Conf or strWarning then -- Confirmation required or Warning to report
local strButton = nil
strButton, strNewVal = SearchReplacePrompt(ptrItem,strOldVal,strNewVal,strWarning)
if strButton == TblOption.Set.Mode then
doManageString(strOldVal,strNewVal) -- V1.7 call new function
elseif strButton ~= "Skip" then
return false -- Process cancelled
end
else
TblOption.Set.Data = general.BuildDataRef(ptrItem)
doManageString(strOldVal,strNewVal) -- V1.7 call new function
end
end
return true
end -- function DoSeekMatch
function strScopeType(strScope) -- Extract Search Scope record type tag called from DoAllItems, SetScopeMode, btnSomeRecs:action -- V2.8
return strScope:match("^(_?%u+)")
end -- function strScopeType
-- Search All Data Items --
function DoAllItems()
local isContinue = true
local intItem = tonumber(TblOption.Set.Item) -- Number of items in database
local tblTgl = TblOption.Tgl -- Keyword array of Mode & Class & Tag boolean toggles
local strScope = TblOption.Lst.Scope -- Search Scope filter -- V2.3
local arrScope = { }
if #strScope == 0 then
for strType in iterate.RecordTypes() do -- Search Scope record type tags are every type with _PLAC first -- V3.2
if strType == "_PLAC" then
table.insert(arrScope,1,strType) -- This ensures Place record Place names get changed first
else
table.insert(arrScope,strType)
end
end
else
local strType = strScopeType(strScope) -- Search Scope record type tag -- V2.3 -- V2.4 -- V2.8
intItem = tonumber(TblOption.Set[strType]) or 0 -- Number of items in record type -- V2.3
table.insert(arrScope,strType)
strScope = "^"..strScope:plain().."%." -- Search Scope filter tag(s) pattern -- V2.3
end
local tblUser = iup_gui.DialogueAttributes("Main")
tblUser.X = tblUser.CoordX+100 -- Popup Progress Bar in same place as Main dialogue
tblUser.Y = tblUser.CoordY+150
progbar.Setup( tblUser )
if intItem > 5000 then
progbar.Start("Searching",intItem) -- Start Progress Bar if enough items in database/record type
end
intItem = 0 -- V2.3
for _, strType in ipairs( arrScope ) do -- Loop through all record types in Scope -- V3.2
local strRecRoot = strType.."." -- Record. tag for item Search Scope -- V2.3
local tblRecs = TblOption.Set.Recs
local useItem = true -- Is data item in Select Records filter? -- V2.8
progbar.Message("Searching "..strType.." Records")
for ptrItem in iterate.Items(strType) do -- Loop through all data items
if tblRecs then
local intRid = fhGetRecordId(ptrItem) -- Select Records filter exists so check Record Id -- V2.8
if intRid > 0 then useItem = tblRecs[intRid] end -- Adjust flag only on record level data items -- V2.8
end
if useItem then -- V2.8
local strChild = fhGetTag(ptrItem) -- Obtain child item Tag
local ptrParent = fhNewItemPtr()
ptrParent:MoveToParentItem(ptrItem) -- Obtain parent item pointer
local strParent = fhGetTag(ptrParent) -- Obtain parent item Tag -- V2.3
if strParent == strType then
strRecRoot = strType.."."..strChild.."." -- Record.Root. tags for Search Scope -- V2.3
end
if strRecRoot:match(strScope) then -- Item matches Search Scope filter -- V2.3
if fhIsFact(ptrParent) then
strParent = "~Fact." -- Parent is any Fact Tag
elseif fhGetDisplayText(ptrParent):match("^LDS %l%l") then
strParent = "~LDS." -- Parent is any LDS Ordinance
else
strParent = strParent.."." -- Otherwise use parent item Tag -- V2.3
ptrParent:MoveToParentItem(ptrParent) -- Prefix "~" to distinguish from Parent=Record
if ptrParent:IsNotNull() then strParent = "~"..strParent end
end
local strParentChild = strParent..strChild -- Parent.Child Tags
if fhIsAttribute(ptrItem) then strChild = "Attr" end -- Special case for all Attributes
local strDataClass,strChild = GetDataClass(ptrItem,strChild) -- Fix for metafields -- V3.4 -- V3.8
local strValueType = fhGetValueType(ptrItem) -- Only text & date fields are supported
if ( strValueType:match("text$") or strValueType == "date" or strValueType == "age" ) -- V3.1 match("text$") for "text" & "richtext" and add "age" for FH V7
and ( tblTgl[strChild] -- Child item Tag or V2.0 "Attr" ?
or tblTgl[strParentChild] -- V2.0 & V2.1 Parent.Child item Tags ?
or ( tblTgl[strDataClass] and -- V2.0 Class match and
tblTgl[strChild] == nil and -- Child and Parent.Child without any toggle option ?
tblTgl[strParentChild] == nil )
) then
isContinue = DoSeekMatch(ptrItem,strDataClass) -- Try string value match for matching data item type
end
end
if not isContinue then break end
if progbar.Stop() then break end
intItem = intItem + 1
if intItem > 99 then -- 1 Jan 2014
progbar.Step(100) -- Step progress bar every 100 items
intItem = 0
collectgarbage("step",0) -- Memory garbage collection to avoid 'Not responding' -- V3.4
end
end -- V2.8
end
end
progbar.Close()
end -- function DoAllItems
function CopyPreset(tblSource,tblTarget) -- Copy current options to/from chosen preset -- V2.5
if not tblSource then tblSource = {} end
if not tblTarget then tblTarget = {} end
for strHead, tblHead in pairs (tblSource) do
if type(strHead) == "string"
and strHead ~= "Set" then -- Only copy "Val", "Tgl", "Lst" dictionary tables
if not tblTarget[strHead] then
tblTarget[strHead] = {}
end
for strData, anyData in pairs (tblHead) do
tblTarget[strHead][strData] = anyData
end
end
end
end -- function CopyPreset
-- Manage Presets GUI -- -- V2.5
function ManagePresets()
local intCurrent = TblOption.Set.Curr -- Save currently selected Preset from Main GUI
local lstProject = iup.list { VisibleColumns=9; Multiple="Yes"; } -- Create GUI controls
local lstProgram = iup.list { VisibleColumns=9; Multiple="Yes"; } -- V2.6 Multiple="Yes"
local lstPhantom = iup.list { VisibleColumns=9; Multiple="Yes"; } -- V2.6 dummy for Export/Import
local btnSaveAll = iup.button{ Title=">> Save >>"; }
local btnLoadAll = iup.button{ Title="<< Load <<"; }
local btnExport = iup.button{ Title="Export" ; } -- V2.6 and other button names shortened
local btnImport = iup.button{ Title="Import" ; } -- V2.6
local btnMoveUp = iup.button{ Title="Move Up"; }
local btnMoveDn = iup.button{ Title="Move Down"; }
local btnUnlock = iup.button{ Title="Unlock" ; }
local btnDelete = iup.button{ Title="Delete" ; }
local lblPreset = iup.label { Title="New Preset Name :"; Alignment="ACENTER:ABOTTOM"; } -- V2.7
local txtPreset = iup.text { Value=""; }
local btnInsert = iup.button{ Title="Insert" ; }
local btnClone = iup.button{ Title="Clone" ; }
local btnRename = iup.button{ Title="Rename" ; }
local btnDestroy = iup.button{ Title="Finished"; action=function(self) return iup.CLOSE end; }
local frmProject = iup.frame { Title=" Project List "; lstProject; }
local frmProgram = iup.frame { Title=" Program List "; lstProgram; }
local boxManager = iup.vbox { Homogeneous="Yes"; btnSaveAll; btnLoadAll; btnExport; btnImport; btnMoveUp; btnMoveDn; btnUnlock; btnDelete; lblPreset; txtPreset; btnInsert; btnClone; btnRename; btnDestroy; } -- V2.6
local frmManager = iup.frame { Title=" Manage Presets "; boxManager; }
local boxPresets = iup.hbox { Homogeneous="Yes"; frmProject; frmManager; frmProgram; }
local dlgPresets = iup.dialog{ Title=iup_gui.Plugin..iup_gui.Version; Gap=iup_gui.Gap; Margin="4x4"; boxPresets; }
-- Set other GUI control attributes
for iupName, tblAttr in pairs ( {
-- Control = 1~FgColor ; 2~Font ; 3~Tip ;
[frmProject] = { iup_gui.Head; iup_gui.FontHead; };
[lstProject] = { iup_gui.Body; iup_gui.FontBody; "Select any local Project Presets in list"; };
[frmProgram] = { iup_gui.Head; iup_gui.FontHead; };
[lstProgram] = { iup_gui.Body; iup_gui.FontBody; "Select any global Program Presets in list"; };
[frmManager] = { iup_gui.Head; iup_gui.FontHead; };
[btnSaveAll] = { iup_gui.Warn; iup_gui.FontBody; "Copy selected Project Presets to Program List"; };
[btnLoadAll] = { iup_gui.Warn; iup_gui.FontBody; "Copy selected Program Presets to Project List"; };
[btnExport] = { iup_gui.Warn; iup_gui.FontBody; "Export any selected Presets to a File" ; }; -- V2.6
[btnImport] = { iup_gui.Warn; iup_gui.FontBody; "Import from a File to the Project List"; }; -- V2.6
[btnMoveUp] = { iup_gui.Safe; iup_gui.FontBody; "Move the selected Preset up the list" ; };
[btnMoveDn] = { iup_gui.Safe; iup_gui.FontBody; "Move the selected Preset down the list"; };
[btnUnlock] = { iup_gui.Risk; iup_gui.FontBody; "Unlock the selected Preset to allow deletion"; };
[btnDelete] = { iup_gui.Risk; iup_gui.FontBody; "Delete the selected Preset from the list"; };
[lblPreset] = { iup_gui.Body; iup_gui.FontBody; "Enter new Preset name for Insert, Clone, or Rename"; };
[txtPreset] = { iup_gui.Safe; iup_gui.FontBody; "Enter new Preset name for Insert, Clone, or Rename"; };
[btnInsert] = { iup_gui.Safe; iup_gui.FontBody; "Insert a blank Preset using New Preset Name"; };
[btnClone ] = { iup_gui.Safe; iup_gui.FontBody; "Clone selected Preset using New Preset Name"; };
[btnRename] = { iup_gui.Safe; iup_gui.FontBody; "Rename selected Preset using New Preset Name"; };
[btnDestroy] = { iup_gui.Risk; iup_gui.FontBody; "Finished Managing Presets"; };
} ) do
iupName.Expand = "Yes"
iupName.FgColor = tblAttr[1]
iupName.Font = tblAttr[2]
iupName.Tip = tblAttr[3]
iupName.TipBalloon = iup_gui.Balloon
end
local function intPresetChosen(lstPreset) -- Return number of selected items in Preset List
local _,intChosen = lstPreset.Value:replace("+","+")
return intChosen
end -- local function intPresetChosen
local function intPresetFirst(lstPreset) -- Return position of first selected item in Preset List
return lstPreset.Value:find("+",1,true) or 0
end -- local function intPresetFirst
local function strPresetValue(lstPreset,intPreset) -- Return list value for a Preset List
local strValue = ""
local intCount = tonumber(lstPreset.Count) or 0
for intValue = 1, math.max(intCount,intPreset or 0) do
if intValue == intPreset then -- Compose list control value string
strValue = strValue.."+"
else
strValue = strValue.."-"
end
end
if lstPreset == lstProject and intPreset then
intCurrent = intPreset -- Update current selected Project Preset
end
return strValue
end -- local function strPresetValue
local function isLocked(intValue,tblOption) -- Is current Preset locked?
if intValue > 0 then
return ( tblOption.Set.List[intValue].Lock == "ON" )
end
return false
end -- local function isLocked
local function newPreset(intValue,tblOption,strValue) -- Is new Preset value unique in chosen list
if intValue > 0 then
for intList, dicList in ipairs (tblOption.Set.List or {}) do
if dicList.Name == strValue then return false end -- No it already exists in list
end
end
return true
end -- local function newPreset
local function doDisableButtons() -- Set button controls inactive
for intName = 1, 20 do
local iupName = boxManager[intName]
if iupName == btnDestroy then break end -- Make each button inactive except btnDestroy
if iup.ClassMatch(iupName,"button") then iupName.Active = "No" end
end
end -- local function doDisableButtons
local function doEnableButtons() -- Set button controls active status
local intProjMax = tonumber(lstProject.Count) or 0
local intProgMax = tonumber(lstProgram.Count) or 0 -- Total count of presets in both lists
local intProject = intPresetChosen(lstProject)
local intProgram = intPresetChosen(lstProgram) -- Total selected presets in both lists
local intProj1st = intPresetFirst(lstProject)
local intProg1st = intPresetFirst(lstProgram) -- First selected preset in both lists
doDisableButtons()
if intProject > 0 then btnSaveAll.Active = "Yes" end -- If any selected preset then activate Save/Load buttons
if intProgram > 0 then btnLoadAll.Active = "Yes" end
if (intProject+intProgram) > 0 then btnExport.Active = "Yes" end -- Activate Export and Import buttons -- V2.6
btnImport.Active = "Yes"
if (intProject+intProgram) == 1 then -- If one selected preset in either list
if isLocked(intProj1st,TblOption)
or isLocked(intProg1st,ArrOption) then
btnUnlock.Active = "Yes" -- Activate Unlock button
else
btnDelete.Active = "Yes" -- Activate Delete button
end
if #txtPreset.Value > 0
and newPreset(intProj1st,TblOption,txtPreset.Value)
and newPreset(intProg1st,ArrOption,txtPreset.Value) then -- If new Preset name is valid
btnInsert.Active = "Yes"
btnClone .Active = "Yes" -- Activate Insert/Clone/Rename buttons
btnRename.Active = "Yes"
end
if intProg1st > 1 -- If not first preset selected then activate Move Up button
or intProj1st > 1 then btnMoveUp.Active = "Yes" end
if ( intProg1st > 0 and intProg1st < intProgMax ) -- If not last preset selected then activate Move Down button
or ( intProj1st > 0 and intProj1st < intProjMax ) then btnMoveDn.Active = "Yes" end
end
end -- local function doEnableButtons
function lstProject:action(strText,intItem,intState) -- Select presets from Project List
lstProgram.Value = strPresetValue(lstProgram) -- Deselect presets in Program List
intCurrent = intPresetFirst(lstProject) -- Update current selected Project Preset
doEnableButtons()
end -- function lstProject:action
function lstProgram:action(strText,intItem,intState) -- Select presets from Program List
lstProject.Value = strPresetValue(lstProject) -- Deselect presets in Project List
doEnableButtons()
end -- function lstProgram:action
local function doCopyOne(lstSource,tblSource,lstTarget,tblTarget) -- Copy one Preset from source to target list
local strPreset = nil -- V2.7
local intSource = intPresetFirst(lstSource)
if intSource > 0 then
local arrSource = tblSource.Set.List
local arrTarget = tblTarget.Set.List
local intTarget = 0
local strSource = arrSource[intSource].Name
for intList, dicList in ipairs (arrTarget) do -- Find source preset name in target list
if strSource == dicList.Name then
intTarget = intList
break
end
end
if intTarget == 0 then -- No target name found so insert new one
lstTarget.AppendItem = strSource
table.insert(arrTarget,{})
intTarget = #arrTarget
end
lstSource.Value = strPresetValue(lstSource)
lstTarget.Value = strPresetValue(lstTarget,intTarget) -- Select target preset as a visual cue
local intAns = 1
if arrTarget[intTarget].Lock == "ON" then
intAns = iup_gui.WarnDialogue("Preset Locked","\n Preset named '"..strSource.."' is Locked. \n\n Continue to overwrite this Preset? \n","Yes","No")
end
if intAns == 1 then
arrTarget[intTarget].Name = strSource
arrTarget[intTarget].Lock = arrSource[intSource].Lock
tblTarget[intTarget] = {}
CopyPreset(tblSource[intSource],tblTarget[intTarget]) -- Copy the preset options
strPreset = strSource -- V2.7
end
end
return strPreset -- V2.7
end -- local function doCopyOne
local function doCopyAll(lstSource,tblSource,lstTarget,tblTarget)
local arrPreset = {}
local strSource = lstSource.Value -- Remember original selected presets
local strTarget = lstTarget.Value
for intSource = 1, #tblSource do -- Loop through source list
if strSource:sub(intSource,intSource) == "+" then
lstSource.Value = strPresetValue(lstSource,intSource) -- Copy the preset options slowly
fhSleep(100,50)
table.insert(arrPreset,doCopyOne(lstSource,tblSource,lstTarget,tblTarget)) -- V2.7 arrPreset
fhSleep(100,50)
end
end
lstSource.Value = strSource -- Restore original selected presets
lstTarget.Value = strTarget
return arrPreset
end -- local function doCopyAll
function btnSaveAll:action() -- Save All presets button action
doDisableButtons()
doCopyAll(lstProject,TblOption,lstProgram,ArrOption)
doEnableButtons()
end -- function btnSaveAll:action
function btnLoadAll:action() -- Load All presets button action
doDisableButtons()
doCopyAll(lstProgram,ArrOption,lstProject,TblOption)
doEnableButtons()
end -- function btnLoadAll:action
local function doExport(lstSource,tblSource) -- Export selected Presets to File
if intPresetChosen(lstSource) > 0 then
local filedlg = iup.filedlg{ dialogtype="SAVE"; Title="Choose Presets Export File"; directory=iup_gui.PublicPath; file="Presets.fhdata"; extfilter="Preset files (*.fhdata)|*.fhdata|"; }
filedlg:popup(iup.CENTER,iup.CENTER)
if filedlg.status ~= "-1" then -- File chosen OK
local strFileName = filedlg.value
local strFolder, strExport = general.SplitFilename(strFileName)
if general.FlgFolderWrite(strFolder) then -- If chosen folder is writeable then all OK
fhSleep(200,100)
local tblExport = { Set={ List={}; }; }
doCopyAll(lstSource,tblSource,lstPhantom,tblExport) -- Save selected Presets to Export table in File
table.save(tblExport,strFileName)
lstPhantom.RemoveItem = "ALL"
fhSleep(200,100)
else
iup_gui.WarnDialogue("Export Failed","\n Export file '"..strExport.."' cannot be saved to folder. \n")
end
end
end
end -- local function doExport
function btnExport:action() -- Export presets button action -- V2.6
doDisableButtons()
doExport(lstProject,TblOption)
doExport(lstProgram,ArrOption)
doEnableButtons()
end -- function btnExport:action
local function doImport(strFileName,strImport) -- Load Import table from File -- V2.7
local tblImport, strError = table.load(strFileName)
if strError then
iup_gui.WarnDialogue("Import Error","\n Import File '"..strImport.."' Load Error: \n"..strError.."\n")
return nil
end
for intImport, arrImport in ipairs (tblImport) do
if not (arrImport.Tgl and arrImport.Lst and arrImport.Val)
or not (tblImport.Set and tblImport.Set.List)
or not (tblImport.Set.List[intImport] and tblImport.Set.List[intImport].Name and tblImport.Set.List[intImport].Lock) then
iup_gui.WarnDialogue("Import Error","\n Import File '"..strImport.."' Format Error. \n")
return nil
end
end
return tblImport
end -- local function doImport
function btnImport:action() -- Import presets button action -- V2.6
doDisableButtons()
local filedlg = iup.filedlg{ dialogtype="OPEN"; Title="Choose Presets Import File"; directory=iup_gui.PublicPath; extfilter="Preset files (*.fhdata)|*.fhdata|All files (*.*)|*.*|"; }
filedlg:popup(iup.CENTER,iup.CENTER)
if filedlg.status ~= "-1" then -- File chosen OK
local strFileName = filedlg.value
if general.FlgFileExists(strFileName) then
local strFolder, strImport = general.SplitFilename(strFileName)
local tblImport = doImport(strFileName,strImport) -- Load & check Import table from File -- V2.7
if tblImport then
fhSleep(200,100)
lstPhantom.Value = string.rep("+",#tblImport) -- Copy Import table to Project List -- V2.7
local arrPreset = doCopyAll(lstPhantom,tblImport,lstProject,TblOption)
fhSleep(200,100)
iup_gui.MemoDialogue("\n File '"..strImport.."' imported "..#arrPreset.." Presets. \n\n"..table.concat(arrPreset,"\n").."\n") -- V2.7
end
end
end
doEnableButtons()
end -- function btnImport:action
local function doMove(lstName,intDir,tblOption) -- Move preset up/down one position
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local tblPreset = tblOption.Set.List
local intNext1 = intValue + intDir -- New position is one above/below chosen preset
local strTemp = lstName[intValue]
lstName[intValue] = lstName[intNext1] -- Swap GUI list entries
lstName[intNext1] = strTemp
local tblTemp = tblPreset[intValue]
tblPreset[intValue] = tblPreset[intNext1] -- Swap associated table items
tblPreset[intNext1] = tblTemp
lstName.Value = strPresetValue(lstName,intNext1) -- Select the moved preset
tblTemp = {}
CopyPreset(tblOption[intValue],tblTemp)
CopyPreset(tblOption[intNext1],tblOption[intValue]) -- Swap the preset options
CopyPreset(tblTemp,tblOption[intNext1])
end
end -- local function doMove
function btnMoveUp:action() -- Move preset Up button action
doMove(lstProject,-1,TblOption)
doMove(lstProgram,-1,ArrOption)
doEnableButtons()
end -- function btnMoveUp:action
function btnMoveDn:action() -- Move preset Down button action
doMove(lstProject,1,TblOption)
doMove(lstProgram,1,ArrOption)
doEnableButtons()
end -- function btnMoveDn:action
local function doUnlock(lstName,tblOption) -- Unlock any preset from list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
tblOption.Set.List[intValue].Lock = "OFF"
end
end -- local function doUnlock
function btnUnlock:action() -- Unlock preset button action
doUnlock(lstProject,TblOption)
doUnlock(lstProgram,ArrOption)
doEnableButtons()
end -- function btnUnlock:action
local function doDelete(lstName,tblOption) -- Delete any preset from list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local arrPreset = tblOption.Set.List
lstName.RemoveItem = intValue -- Remove selected Preset from GUI list & associated table
table.remove(arrPreset,intValue)
local intCount = #arrPreset
for intItem = intValue, intCount do -- Move the preset options up to close gap
CopyPreset(tblOption[intItem+1],tblOption[intItem])
end
tblOption[intCount+1] = nil
tblOption[intCount+2] = nil
intValue = math.max(1,intValue-1) -- Select 1st preset or preset above removed preset
lstName.Value = strPresetValue(lstName,intValue)
end
end -- local function doDelete
function btnDelete:action() -- Delete preset button action
doDelete(lstProject,TblOption)
doDelete(lstProgram,ArrOption)
doEnableButtons()
end -- function btnDelete:action
function txtPreset:valuechanged_cb() -- Set buttons after preset name changed
doEnableButtons()
end -- function txtPreset:valuechanged_cb
local function doInsert(lstName,tblOption,btnAction) -- Insert/Clone new preset into list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local arrPreset = tblOption.Set.List
local strPreset = txtPreset.Value
intValue = intValue + 1
lstName["InsertItem"..intValue] = strPreset -- Insert new Preset name into GUI list & associated table
table.insert(arrPreset,intValue,{Name=strPreset;Lock="OFF";})
local intCount = #arrPreset
tblOption[intCount] = {}
for intItem = intCount, intValue, -1 do -- Move the preset options down
CopyPreset(tblOption[intItem-1],tblOption[intItem]) -- Leave chosen options in clone
end
if btnAction == btnInsert then
UserDefaultOptions(tblOption[intValue]) -- Reset the options for insert
end
txtPreset.Value = ""
lstName.Value = strPresetValue(lstName,intValue) -- Select inserted/cloned preset
end
end -- local function doInsert
function btnInsert:action() -- Insert preset button action
doInsert(lstProject,TblOption,btnInsert)
doInsert(lstProgram,ArrOption,btnInsert)
doEnableButtons()
end -- function btnInsert:action
function btnClone:action() -- Clone preset button action
doInsert(lstProject,TblOption,btnClone)
doInsert(lstProgram,ArrOption,btnClone)
doEnableButtons()
end -- function btnRename:action
local function doRename(lstName,tblOption) -- Rename any preset in list
local intValue = intPresetFirst(lstName)
if intValue > 0 then
local arrPreset = tblOption.Set.List
local strPreset = txtPreset.Value
lstName [intValue] = strPreset -- Put new Preset name in GUI list & associated table
arrPreset[intValue].Name = strPreset
txtPreset.Value = ""
lstName.Value = strPresetValue(lstName,intValue) -- Select renamed preset
end
end -- local function doRename
function btnRename:action() -- Rename preset button action
doRename(lstProject,TblOption)
doRename(lstProgram,ArrOption)
doEnableButtons()
end -- function btnRename:action
for intList, dicList in ipairs (TblOption.Set.List or {}) do -- Load the Project Preset list names
lstProject[intList] = dicList.Name
end
for intList, dicList in ipairs (ArrOption.Set.List or {}) do -- Load the Program Preset list names
lstProgram[intList] = dicList.Name
end
lstProject.Value = strPresetValue(lstProject,intCurrent) -- Select same Preset as in main GUI
lstProgram.Value = strPresetValue(lstProgram)
doEnableButtons()
iup_gui.ShowDialogue("Preset",dlgPresets,btnDestroy,"Normal Keep") -- Popup dialogue that remembers its attributes & refreshes its size
TblOption.Set.Curr = intCurrent -- Update currently selected Preset in Main GUI
end -- function ManagePresets
-- Main GUI Dialogue --
function GUI_MainDialogue()
--[[
TblOption.Set has these entries for global option settings :
Item Item count of database size for search progress bar
Date Date of last Item update to avoid recalculation run-time
List Droplist of the Project Preset names
Curr Current chosen Project Preset
Mode "Replace" = Search & Replace or "Report" = Search ONLY (not sticky)
Data Data reference for currently matched item (not sticky)
TblOption.Val has these values for search & replace strings :
Seek Search text string
Swap Replace text string
TblOption.Tgl and TblOption.Lst and other tables below may have these entries for "ON"/"OFF" toggles or combo drop list Tags :
Mixed case entries are Mode settings :
Text Plain Text Mode v. LUA Pattern Text Mode
Case Case Insensitive v. Case Sensitive
Word Whole Words v. Part Words
WhSp Hide v. Show white space symbols -- V3.0
Recs All Records v. Chosen Records -- V2.8
Scope Set Search Scope combo drop list -- V2.3
Conf Confirm Text Replacements
Attr Attribute Values enabled/disabled -- V2.0
Age Age Error checks enabled/disabled -- V2.1
Date Date Warnings enabled/disabled -- V1.7
Phrase Date Phrase Warnings enabled/disabled -- V2.2
Lower case entries are single Data Class names such as "text", "name", "place" and "longtext"
Upper case entries are single Gedcom Tag names such as "NICK", "FORM", "_PLAC" and "_EMAIL"
Upper case entries separated by a dot are Record parent & child Tag names such as "SOUR.TITL"
Upper case entries prefixed by a tilda "~" are non-Record parent & child such as "~DATA.TEXT"
These latter are used where similar Gedcom Tag names need to be differentiated :
NOTE.TEXT (Note record Text) versus ~_FIELD.TEXT & SOUR.TEXT & ~DATA.TEXT (Text from Source) all "longtext" or "richtext" in FH V7
INDI.NAME "name" (Individual Name) versus ~_FIELD.NAME & REPO.NAME & SUBM.NAME "text" (Metafield Name & Repository/Submitter record Name c.f. Source/Media record Title)
INDI.TITL "text" (Title Attribute) versus SOUR.TITL "longtext" & SOUR.ABBR "text" & OBJE.TITL "text" (Source/Media record Title)
Special variants are ~Fact.NOTE & ~Fact.DATE for any Fact parent tags, and ~LDS.NOTE & ~LDS.DATE for any LDS Ordinance parent tags.
--]]
local tblSet = TblOption.Set -- Local shortcuts to global option tables
local tblVal = TblOption.Val
local tblTgl = TblOption.Tgl
local tblLst = TblOption.Lst
local tblBasic = { } -- GUI basic filter toggle iup controls
local tblExtra = { } -- GUI extra filter toggle iup controls
local tblCombo = { } -- GUI combo filter drop list iup controls
local tblRelated = { -- Related GUI settings dependancies
-- Short Text fields: "text", "word", "name", "place", etc...
text = { "word"; "word2"; "latlong"; "enumword"; "wordlist"; };-- FORM = "word"; ROLE & TYPE = "word2"; LATLONG = "latlong"; QUAY, PEDI, etc = "enumword"; "wordlist"?; remainder are "text", except NAME/"name" & PLAC/"place" below -- V3.1
name = { "NPFX"; "NSFX"; "NICK"; "GIVN"; "_USED"; "SURN"; "SPFX"; "~NAME.FONE"; "~NAME.ROMN"; "~_FIELD.NAME"; }; -- V3.1 add FONE & ROMN -- V3.8 add ~_FIELD.NAME
PLAC = { "place"; "_PLAC"; "~PLAC.FONE"; "~PLAC.ROMN"; "~_FIELD.PLAC"; }; -- V3.1 add FONE & ROMN -- V3.8 add ~_FIELD.PLAC
RFN = { "RIN" ; };
FORM = { }; -- V2.0 FORM is "word" for OBJE.FORM & OBJE.FILE.FORM and PLAC.FORM
FILE = { "_FILE"; }; -- V2.9 add FILE
["~_FIELD.TEXT"]= { "~_FIELD.NAME"; "~_FIELD.DATE"; "~_FIELD.PLAC"; "~_FIELD.ENUM"; "~_FIELD.URL"; }; -- V3.1 -- "name", "date", "place", "text", "url"
PHON = { "EMAIL"; "WWW" ; "_WEB"; "_EMAIL"; }; -- V3.1 add EMAIL & WWW
ADR1 = { "ADR2" ; "ADR3"; "CITY"; "STAE"; "POST"; "CTRY"; }; -- V3.1 add ADR3
TEMP = { "AFN" ; "FAMF"; }; -- INDI.CONL.TEMP & SUBN.TEMP & INDI.AFN & SUBN.FAMF
_SRCT = { "_SRCT.NAME"; "_SRCT.COLL"; "_SRCT.CATG"; "_SRCT.SUBC"; "_SRCT.TITL"; "_SRCT.BIBL"; "_SRCT.FOOT"; "_SRCT.SHRT"; "~FDEF.NAME"; "~FDEF.CODE"; "~FDEF.TYPE"; "~FDEF.PROM"; }; -- V3.1 -- V4.1
-- Long Text fields: "longtext" or "richtext" in FH V7
NOTE2 = { "_NOTE"; "_NOTA"; "_SRCT.DESC"; "~FDEF.DESC" }; -- V3.1 add _NOTA & _SRCT.DESC & ~FDEF.DESC
["SOUR.TEXT"] = { "~DATA.TEXT"; };
["NOTE.TEXT"] = { "_RNOT.TEXT"; }; -- V3.1 add _RNOT.TEXT
ADDR = { "~_FIELD.ADDR"; }; -- V3.8 add ~_FIELD.ADDR metafield
-- Distinctive fields: "text" & "longtext" & "richtext" mix & "date"
["SOUR.TITL"] = { "SOUR.ABBR"; "OBJE.TITL"; "~FILE.TITL"; "REPO.NAME"; "_PLAC.TEXT"; "_PLAC.STAN"; "_SRCT.NAME"; "SUBM.NAME"; }; -- V3.1 add ~FILE.TITL & _PLAC.TEXT & _SRCT.NAME -- V3.5 add _PLAC.STAN
}
local function addFilterItem(arrFilt,strName,strTitle,strTip_T,strTip_L,...) -- Add a filter item -- V3.8
-- arrFilt ~ Filter table
-- strName ~ Name of filter
-- strTitle ~ Title of filter in GUI
-- strTip_T ~ Tip for toggle
-- strTip_L ~ Tip for drop list
-- ... ~ Drop list items
local dicFilt = { }
if strName == "Scope" then -- Search Scope special case
arrFilt.Name = strName
arrFilt.Tip_L = strTip_L
dicFilt = arrFilt
else
dicFilt = { Name=strName; Title=strTitle; Tip_T=strTip_T; Tip_L=strTip_L; }
end
for intItem, strItem in ipairs ({...}) do -- Set index value for each drop list item
table.insert(dicFilt,strItem)
if not dicFilt.Item then dicFilt.Item = {} end
strItem = strItem:gsub(" %(.-%)$","")
dicFilt.Item[strItem] = intItem
end
if strName ~= "Scope" then
table.insert(arrFilt,dicFilt)
if not arrFilt.Item then arrFilt.Item = {} end
arrFilt.Item[strName] = #arrFilt
end
return arrFilt[#arrFilt]
end -- local function addFilterItem
local tblFilters = { } -- GUI basic, extra & combo filter attributes -- V2.1
tblFilters.SearchScope = { fhFunc=fhIsFact; tblTag={}; Cols=20; } -- Search Scope special combo drop list -- V2.3
tblFilters.SearchScope.Item = { }
local tblScope = tblFilters.SearchScope
addFilterItem( tblScope, "Scope", nil, nil, "Set global search scope for Basic/Extra Filters",
"All Records & Events/Attributes", "Individual Records (INDI)", "Family Records (FAM)", "Note Records (NOTE)", "Source Records (SOUR)", "Repository Records (REPO)", "Multimedia Records (OBJE)", "Submitter Records (SUBM)", "Submission Records (SUBN)" )
local intScope = tblScope.Item["Submitter Records"]
if fhGetAppVersion() > 5 then -- FH V6 record scope -- V2.4/5
table.insert(tblScope,intScope,"Place Records (_PLAC)") -- Insert Place Records item before Submitter Records item
end
if fhGetAppVersion() > 6 then -- FH V7 record scope -- V3.1
table.insert(tblScope,intScope+1,"Research Note Records (_RNOT)") -- Insert these two after Place Records item
table.insert(tblScope,intScope+2,"Source Template Records (_SRCT)")
end
-- Name of filter= Title & Tip for toggle used in two filter tables;
tblFilters.name = { Title="Individual Names, Prefix, Suffix, Given, etc";
Tip_T="All Individual Primary and Alternate Name (NAME) fields including: \n (NPFX) Name Prefix, \n (NSFX) Name Suffix, \n (NICK) Nickname, \n (GIVN) Given Name, \n (_USED) Given Name Used, \n (SURN) Surname, \n (SPFX) Surname Prefix"; }
tblFilters.PLAC = { Title="All the Place fields"; -- V2.5 -- V3.8
Tip_T="Fact Place (PLAC) and To/From (_PLAC) and Source Event Place (PLAC) fields"; }
tblFilters.PAGE = { Title="Where Within Source fields in Citations";
Tip_T="Where Within Source (PAGE) fields in Citations"; }
tblFilters.NOTE2 = { Title="All the Note fields";
Tip_T="All the Note and Link/Note (NOTE/_NOTE) fields"; }
tblFilters.ADDR = { Title="All the Address fields"; -- V2.5 -- V3.8
Tip_T="Fact, Repository & Submitter Address (ADDR) fields"; }
tblFilters.TEXT = { Title="Text from Source fields";
Tip_T="Text from Source (TEXT) fields in Citations and Sources"; }
tblFilters.date = { Title="All the Date fields"; -- V2.5 moved
Tip_T="Fact, Citation Entry, Multimedia, and LDS Date (DATE/_DATE) fields"; } -- Fact DATE, Citation DATA.DATE, Multimedia OBJE._DATE, LDS Ordination BAPL/CONL/ENDL/SLGC/SLGS.DATE fields
if fhGetAppVersion() > 6 then -- FH V7 variants -- V3.1
tblFilters.name .Tip_T = tblFilters.name.Tip_T..", \n (FONE) Phonetic Name, \n (ROMN) Romanized Name, \n (~_FIELD.NAME) Templated Source Metafields" -- Source Metafield ~_FIELD.NAME -- V3.8
tblFilters.PLAC .Tip_T = tblFilters.PLAC.Tip_T..", \n Phonetic Place (FONE) and Romanized Place (ROMN) subfields, \n Templated Source Metafields (~_FIELD.PLAC)" -- Source Metafield ~_FIELD.PLAC -- V3.8
tblFilters.NOTE2.Title = "Note && Description fields"
tblFilters.NOTE2.Tip_T = "All local Note (NOTE) and Description (DESC) fields"
tblFilters.ADDR .Tip_T = tblFilters.ADDR.Tip_T:replace("Fact", "Fact, Source Metafield" ) -- Source Metafield ~_FIELD.ADDR -- V3.8
tblFilters.date .Tip_T = tblFilters.date.Tip_T:replace("Entry","Entry, Source Metafield") -- Source Metafield ~_FIELD.DATE
end
tblFilters.BasicShort = { } -- Basic Filter Short Text toggles -- V2.1
local arrFilter = tblFilters.BasicShort
-- Filter , Name , Title , Tip for toggle -- V3.8
addFilterItem( arrFilter , "name" , tblFilters.name.Title , tblFilters.name.Tip_T )
addFilterItem( arrFilter , "PLAC" , tblFilters.PLAC.Title , tblFilters.PLAC.Tip_T ) -- V2.5
addFilterItem( arrFilter , "PAGE" , tblFilters.PAGE.Title , tblFilters.PAGE.Tip_T )
addFilterItem( arrFilter , "text" , "All other Text fields", "All other Short Text and Long Text fields" )
tblFilters.BasicLong = { } -- Basic Filter Long Text toggles -- V2.1
local arrFilter = tblFilters.BasicLong
-- Filter , Name , Title , Tip for toggle -- V3.8
addFilterItem( arrFilter , "NOTE2" , tblFilters.NOTE2.Title , tblFilters.NOTE2.Tip_T )
addFilterItem( arrFilter , "ADDR" , tblFilters.ADDR.Title , tblFilters.ADDR.Tip_T ) -- V2.5
addFilterItem( arrFilter , "SOUR.TEXT", tblFilters.TEXT.Title , tblFilters.TEXT.Tip_T )
addFilterItem( arrFilter , "date" , tblFilters.date.Title , tblFilters.date.Tip_T ) -- V2.5 moved
tblFilters.ShortText = { } -- Extra Filter Short Text toggles & combo drop lists
local arrFilter = tblFilters.ShortText
-- Filter , Name , Title , Tip for toggle , Tip for drop list , Drop list -- V3.8
addFilterItem( arrFilter , "name" , tblFilters.name.Title , tblFilters.name.Tip_T )
local tblPlac = addFilterItem( arrFilter , "PLAC" , tblFilters.PLAC.Title , tblFilters.PLAC.Tip_T , "Select a Place field type" ,
"All the Place fields", "Fact Places (~Fact.PLAC)", "To/From Place (~Fact._PLAC)", "Source Event Place (~EVEN.PLAC)" ) -- V3.8 added Place options
if fhGetAppVersion() > 6 then
table.insert(tblPlac,"Phonetic Place (~PLAC.FONE)") -- FH V7 Add drop list Phonetic, Romanized & Source Metafield -- V3.8
table.insert(tblPlac,"Romanized Place (~PLAC.ROMN)")
table.insert(tblPlac,"Source Metafield (~_FIELD.PLAC)")
end
local tblAttr = addFilterItem( arrFilter , "Attr" , "Fact Attribute values" , "Any value for Fact Attributes" , "Select an Attribute fact" ,
"All the Attribute facts" ) tblAttr.fhFunc=fhIsAttribute tblAttr.tblTag={RESI=true;} -- Append all Attributes with values, so exclude Residence (RESI)
addFilterItem( arrFilter , "CAUS" , "Fact Cause fields" , "Fact Cause (CAUS) fields" )
addFilterItem( arrFilter , "AGNC" , "Fact && Source Responsible Agency fields" , "Fact and Source Responsible Agency (AGNC) fields" )
addFilterItem( arrFilter , "PAGE" , tblFilters.PAGE.Title , tblFilters.PAGE.Tip_T )
addFilterItem( arrFilter , "_TYPE" , "Generic Type fields in Source records" , "Generic Type (_TYPE) fields in Source records" )
if fhGetAppVersion() > 6 then
addFilterItem( arrFilter , "~_FIELD.TEXT", "Templated Source Metafields" , "Templated Source record (_FIELD) Metafields" , "Select a Templated Source Metafield" ,
"All Templated Source Metafields", "Text (~_FIELD.TEXT)", "Name (~_FIELD.NAME)", "Date (~_FIELD.DATE)", "Place (~_FIELD.PLAC)", "Address (~_FIELD.ADDR)", "Enum (~_FIELD.ENUM)", "URL (~_FIELD.URL)" ) -- V3.8
end
local tblType = addFilterItem( arrFilter , "TYPE" , "Type fields for Facts && Custom Id" , "Type (TYPE) fields for Facts and Custom Id" )
if fhGetAppVersion() > 6 then
tblType.Title = tblType.Title.." && Phonetic/Roman Names/Places" tblType.Tip_T = tblType.Tip_T.."\n and Phonetic or Romanized Names or Places"
end
addFilterItem( arrFilter , "REFN" , "Custom Id fields" , "Custom Id (REFN) fields" )
addFilterItem( arrFilter , "RFN" , "Permanent && Automated Record Number fields", "Permanent (RFN) and Automated (RIN) Record Number fields" )
addFilterItem( arrFilter , "FORM" , "Multimedia Format && Place Hierarchy fields", "Multimedia Format and Place Hierarchy (FORM) fields" ) -- V2.9
local tblFile = addFilterItem( arrFilter , "FILE" , "Multimedia Linked File fields && Media files", "Multimedia Linked File (FILE/_FILE) fields \n and associated Media path names" ) -- V2.9
if fhGetAppVersion() > 6 then
tblFile.Tip_T = tblFile.Tip_T:replace("/_FILE","")
end
addFilterItem( arrFilter , "_EXCL" , "Multimedia Exclude flags" , "Multimedia Exclude from Diagrams & Reports (_EXCL) flags" ) -- V3.1
addFilterItem( arrFilter , "_KEYS" , "Multimedia Keyword fields" , "Multimedia Keyword (_KEYS) fields" )
local tblPhon = addFilterItem( arrFilter , "PHON" , "Phone && Email && Website fields" , "All the Phone (PHON) and Email (_EMAIL) and Website (_WEB) fields" )
if fhGetAppVersion() > 6 then
tblPhon.Tip_T = tblPhon.Tip_T:replace("_EMAIL","EMAIL"):replace("_WEB","WWW")
end
local tblAdr1 = addFilterItem( arrFilter , "ADR1" , "Address Line1/2, City, State, Postcode && Country", "All address Line1 (ADR1), Line2 (ADR2), City (CITY), \n State (STAE), Postcode (POST) and Country (CTRY) fields" )
if fhGetAppVersion() > 6 then
tblAdr1.Title = tblAdr1.Title:replace("Line1/2","Line1/2/3") tblAdr1.Tip_T = tblAdr1.Tip_T:replace("(ADR2)","(ADR2), Line3 (ADR3)")
end
addFilterItem( arrFilter , "TEMP" , "LDS Temple && File Number && Filename fields", "LDS Temple Codes (TEMP), Ancestral File Number (AFN)\n and Family Filename (FAMF) fields" )
if fhGetAppVersion() > 6 then
addFilterItem( arrFilter , "_SRCT" , "Source Template record fields " , "Source Template record (_SRCT) short text fields" , "Select a Source Template record short text field" ,
"All Template short text fields", "Name (_SRCT.NAME)", "Collection (_SRCT.COLL)", "Category (_SRCT.CATG)", "Subcategory (_SRCT.SUBC)", "Bibliography (_SRCT.BIBL)", "Footnote (_SRCT.FOOT)", "Short Footnote (_SRCT.SHRT)", "Metafield Name (~FDEF.NAME)", "Metafield Code (~FDEF.CODE)", "Metafield Type (~FDEF.TYPE)", "Metafield Prompt (~FDEF.PROM)" )
end
local tblText = addFilterItem( arrFilter , "text" , "All other one line Short Text fields" , "All other one line Short Text fields including: \n (\"UDF\") Uncategorised Data Fields, \n (_SENT) Fact Customised Sentences, \n (QUAY) Citation Assessments, \n (ROLE) Citation Event Type Roles, \n (RELA) Association Relationships" )
if fhGetAppVersion() > 5 then -- FH V6 ShortText variants -- V2.4
tblText.Tip_T = tblText.Tip_T:replace("Event Type","Event Type & Witness"):replace("Relationships","Relationships, \n (LATLONG) Place Lat/Longitude Fields")
end
--[[ Uncategorised Data Fields (UDF),
Fact Customised Sentences (Fact._SENT,Fact._SHAR._SENT,Fact._SHAN._SENT),
Fact Witness Roles Titles (Fact._SHAR.ROLE,Fact._SHAN.ROLE),
Citation Assessment Types (Fact.SOUR.QUAY/_QUAY enumword),
Citation Event Type Roles (Fact.SOUR.EVEN.ROLE word2),
Association Relationships (INDI.ASSO.RELA),
Family Child Relationship (INDI.FAMC.PEDI/_PEDI enumword),
Place Lat/Longitude Field (_PLAC.MAP.LATLONG, latlong),
Submitter Record Language (SUBM.LANG),
Source Repository Identifications (SOUR.REPO.CALN), etc...
--]]
tblFilters.LongText = { } -- Extra Filter Long Text toggles & combo drop lists
local arrFilter = tblFilters.LongText
-- Filter , Name , Title , Tip for toggle , Tip for drop list , drop list -- V3.8
local tblNote = addFilterItem( arrFilter , "NOTE2" , tblFilters.NOTE2.Title , tblFilters.NOTE2.Tip_T , "Select a Note field type" ,
"All the Note fields", "Fact Notes (~Fact.NOTE)", "Individual (INDI.NOTE)", "Family (FAM.NOTE)", "Citation (~SOUR.NOTE)", "Source (SOUR.NOTE)", "Repository (REPO.NOTE)", "Multimedia (OBJE._NOTE)", "Link/Notes (OBJE.NOTE)", "LDS Ordination (~LDS.NOTE)", "Last Change (~CHAN.NOTE)" )
if fhGetAppVersion() > 5 then -- FH V6 Note variants -- V2.4/5
local intIndi = tblNote.Item["Individual"]
table.insert(tblNote,intIndi ,"Witness Notes (~_SHAR.NOTE)") -- Insert before Individual item
table.insert(tblNote,intIndi+1,"Witness Notes (~_SHAN.NOTE)")
table.insert(tblNote,#tblNote-1,"Place Notes (_PLAC.NOTE)") -- Insert as prepenultimate item
end
if fhGetAppVersion() > 6 then -- FH V7 Note variants -- V3.1
local intMedia = tblNote.Item["Multimedia"]
tblNote[intMedia+2] = "Media Note (OBJE.NOTE)" -- Needs +2 due to inserted Witness Notes items
tblNote[intMedia+3] = "Media Annotation (_NOTA)"
table.insert(tblNote,#tblNote-1,"Source Template (_SRCT.NOTE)") -- Insert as prepenultimate NOTE2 items
table.insert(tblNote,#tblNote-1,"Template Description (_SRCT.DESC)")
table.insert(tblNote,#tblNote-1,"Metafield Description (~FDEF.DESC)")
end
local tblAddr = addFilterItem( arrFilter , "ADDR" , tblFilters.ADDR.Title , tblFilters.ADDR.Tip_T , "Select an Address field type",
"All the Address fields", "Fact Addresses (~Fact.ADDR)", "Repository Address (REPO.ADDR)", "Submitter Address (SUBM.ADDR)" ) -- V3.8 added Address options
if fhGetAppVersion() > 6 then
table.insert(tblAddr,"Source Metafield (~_FIELD.ADDR)") -- FH V7 Add drop list Source Metafield -- V3.8
end
addFilterItem( arrFilter , "SOUR.TEXT", tblFilters.TEXT.Title , tblFilters.TEXT.Tip_T , "Select Citations or Sources",
"Both the field types", "Citation (~DATA.TEXT)", "Source (SOUR.TEXT)" )
addFilterItem( arrFilter , "AUTH" , "Author fields in Source records" , "Author (AUTH) fields in Source records" )
addFilterItem( arrFilter , "PUBL" , "Publication Info fields in Source records" , "Publication Info (PUBL) fields in Source records" )
addFilterItem( arrFilter , "SOUR2" , "Source Note fields" , "Source Note (SOUR) fields" )
addFilterItem( arrFilter , "NOTE.TEXT", "Note record Text fields" , "Note record Text (TEXT) fields" )
tblFilters.Distinct = { } -- Extra Filter Distinct toggles & combo drop lists
local arrFilter = tblFilters.Distinct
-- Filter , Name , Title , Tip for toggle , Tip for drop list , drop list -- V3.8
local tblTitl = addFilterItem( arrFilter , "SOUR.TITL", "Record Names/Titles" , "Source, Repository, Multimedia, Submitter Name/Title (ABBR/NAME/TITL) fields" , "Select Record name/title type" ,
"All the Record types", "Source Title (SOUR.TITL)", "Source Short (SOUR.ABBR)", "Repository (REPO.NAME)", "Multimedia (OBJE.TITL)", "Submitter (SUBM.NAME)" )
if fhGetAppVersion() > 5 then -- FH V6 SOUR.TITL
table.insert(tblTitl,#tblTitl,"Place Name (_PLAC.TEXT)") -- Insert as penultimate item
table.insert(tblTitl,#tblTitl,"Standardized (_PLAC.STAN)") -- Insert as penultimate item -- V3.5
tblTitl.Tip_T = tblTitl.Tip_T:gsub("(Submitter.*NAME)","Place, Standardized, \n %1/TEXT/STAN") -- V3.5
end
if fhGetAppVersion() > 6 then -- FH V7 SOUR.TITL -- V3.1
local intMedia = tblTitl.Item["Multimedia"]
tblTitl[intMedia] = "Multimedia (~FILE.TITL)"
table.insert(tblTitl,#tblTitl,"Source Template (_SRCT.NAME)") -- Insert as penultimate item
tblTitl.Tip_T = tblTitl.Tip_T:replace("Standardized","Standardized, Source Template") -- V3.5
end
addFilterItem( arrFilter , "age" , "Fact Age fields" , "Fact Age (AGE) fields" )
addFilterItem( arrFilter , "radAge_Mode" )
local tblDate = addFilterItem( arrFilter , "date" , tblFilters.date.Title , tblFilters.date.Tip_T , "Select a Date field type" ,
"All the Date fields", "Fact Dates (~Fact.DATE)", "Citation Entry (~DATA.DATE)", "LMO/Sort Date (~OBJE._DATE)", "Multimedia Date (OBJE._DATE)", "LDS Ordination (~LDS.DATE)" ) -- LDS Ordination BAPL/CONL/ENDL/SLGC/SLGS.DATE fields -- V3.0 added LMO/Sort Date (~OBJE._DATE)
if fhGetAppVersion() > 6 then -- FH V7 Dates -- V3.1
local intLMO = tblDate.Item["LMO/Sort Date"]
tblDate[intLMO] = "Local Media Date (~OBJE._DATE)"
table.insert(tblDate,intLMO,"Source Metafield (~_FIELD.DATE)")
end
addFilterItem( arrFilter , "radDateMode" )
addFilterItem( arrFilter , "radPhraseMode" ) -- V2.2
local function strComboTag(strTag) -- Combo drop list tag structure to ensure conformity, and structure must also be used in '2nd/3rd/4th... Item for drop list' above
if strTag then return " ("..strTag..")" end -- Used by doPopulateCombo() to set tag, and setComboValues() to match tag
return " %(([^()]+)%)$" -- Used by setScopeMode(), saveOptions(), and btnSomeRecs:action() to extract tag -- Fix to cope with custom facts like 'An (ODD) Fact (INDI.EVEN-AN_ODD_FACT)' -- V3.6
end -- local function strComboTag
local function doPopulateCombo(tblItem) -- Populate all combo drop list controls with items -- V2.1
local strName = tblItem.Name
local iupList = tblCombo[strName] -- Obtain the iup.list control via its name
iupList.AutoRedraw = "No"
iupList.Sort = "No"
for intTag = 1, #tblItem do -- Populate the combo drop list without Sort, as table items set order -- V2.3
iupList[intTag] = tblItem[intTag]
end
if tblItem.fhFunc then -- Populate the Scope/Attr list with appended FH Facts -- V2.3
local tblTag = tblItem.tblTag -- Dictionary & Array table of Fact Tags found -- V2.3
for intRec, strRec in ipairs ({"INDI";"FAM";}) do -- Find all Individual and Family Facts for combo drop list
progbar.Step() -- Step progress bar -- V2.6
for ptrRec in iterate.Records(strRec) do
for ptrFact in iterate.Facts(ptrRec) do
if tblItem.fhFunc(ptrFact) then -- Event/Attribute fact found -- V2.3
local strTag = fhGetTag(ptrFact)
if not tblTag[strTag] then -- Omit any Event/Attribute already found or excluded
tblTag[strTag] = true
table.insert(tblTag,fhCallBuiltInFunction("FactName",ptrFact)..strComboTag(strRec.."."..strTag))
end
end
end
if progbar.Stop() then error("\n Plugin Aborted. ") end
end
end
table.sort(tblTag) -- Sort the Facts alphabetically and append to the drop list
for intTag = 1, #tblTag do
iupList[ tonumber(iupList.Count) + 1 ] = tblTag[intTag] -- V3.1
end
end
iupList.AutoRedraw = "Yes"
end -- local function doPopulateCombo
local function setComboValues() -- Set values for combo drop list tags, called from btnDefault:action -- V2.1
for strName, iupList in pairs ( tblCombo ) do
local strTag = tblLst[strName] or ""
iupList.Value = 1 -- Default to 1st entry
if strTag ~= "" then
for intList = 2, iupList.Count do -- Search combo drop list items for matching tag
if iupList[intList]:matches(strComboTag(strTag)) then
iupList.Value = intList -- Select chosen entry
break
end
end
end
end
end -- local function setComboValues
local function setToolTip(iupItem1,iupItem2) -- Refresh control tooltip otherwise it vanishes in XP!
iupItem2 = iupItem2 or iupItem1
iupItem1.Tip = iupItem2.Tip
end -- local function setToolTip
local function iupToggle(tblItem,tblTab) -- Return toggle handle created from a filter table item above
local strName = tblItem.Name -- V2.3
local strValue = tblTgl[strName] or "ON"
local iupToggle = iup.toggle { Value=strValue; Title=tblItem.Title; Tip=tblItem.Tip_T; TipBalloon=iup_gui.Balloon; Expand="Yes"; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltip -- V2.3
iupToggle.TipDelay = 6000 + ( #tblItem.Tip_T * 20 ) -- Display larger tooltips for longer -- V2.3
tblTab[strName] = iupToggle -- Save toggle handle in tblBasic or tblExtra
return iupToggle
end -- local function iupToggle
local function iupList(tblItem) -- Return combo drop list handle created from a filter table item above
local strName = tblItem.Name -- V2.3
local intCols = tblItem.Cols or 14 -- Search Scope is wider than default for Extra Filter drop lists -- V2.3 was 9 -- V3.1 is 14
local iupList = iup.list { DropDown="Yes"; VisibleColumns=intCols; Visible_Items=20; Tip=tblItem.Tip_L; TipBalloon=iup_gui.Balloon; Expand="Yes"; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltip -- V2.3
tblCombo[strName] = iupList
doPopulateCombo(tblItem) -- Populate combo drop list with names & tags from filter table item above
return iupList
end -- local function iupList
local function doAppend(boxHandle,strEntry,...) -- Append controls to GUI box using a filter attributes entry above
local arg = {...} -- V3.1
local intArg = 1
local tblTab = tblExtra
if strEntry:match("Basic") then -- Choose tblBasic or tblExtra to save toggle handles
tblTab = tblBasic
end
for intItem, tblItem in ipairs ( tblFilters[strEntry] ) do -- For each filter table entry:
local iupItem = nil
if #tblItem > 0 then -- Append toggle and combo drop list -- V2.1 -- V2.3
iupItem = iup.hbox { iupToggle(tblItem,tblTab); iupList(tblItem); Homogeneous="Yes"; Gap="1"; Margin="2x0"; }
elseif tblItem.Title then -- V2.3
iupItem = iup.hbox { iupToggle(tblItem,tblTab); Margin="2x0"; }-- Append just a toggle
else
if arg[intArg] then
iupItem = arg[intArg] -- Append another control if supplied
intArg = intArg + 1
end
end
if iupItem then iup.Append(boxHandle,iupItem) end -- Append control to GUI box
local strName = tblItem.Name -- V2.3
if tblBasic[strName] and -- Adjust a Basic Filter to 3 State toggle
( strName == "text" or #tblItem > 0 ) then -- if "text" toggle or associated Extra Filter has combo drop list -- V2.3
local iupToggle = tblBasic[strName]
iupToggle["3State"] = "Yes" -- User can only set "ON"=1 or "OFF"=0 but Extra Filters tab can set "NOTDEF"=-1
iupToggle.action = function(self,intState) if intState < 0 then self.Value = "OFF" end setToolTip(self) end -- Refresh XP Tooltip
end
end
end -- local function doAppend
local function iupValue(strValue,tglA,tglB) -- Return toggle handle depending on toggle value
if strValue == "OFF" then
return tglB
else
return tglA
end
end -- local function iupValue
local function iupRadio(strValue,tglA,tglB) -- Return radio handle created from two toggles
return iup.radio { iup.hbox { tglA; tglB; Homogeneous="Yes"; }; Value=iupValue(strValue,tglA,tglB); }
end -- local function iupRadio
local function setAllToggles(strEntry,strMode) -- Set all Short/Long Text toggle options "ON"/"OFF"
for intItem, tblItem in ipairs ( tblFilters[strEntry] ) do
tblExtra[tblItem.Name].Value = strMode -- V2.3
if strMode == "ON" and tblCombo[tblItem.Name] then -- If toggle is ticked and combo drop list exists, -- V2.3
tblCombo[tblItem.Name].Value = 1 -- then reset combo drop list to default 1st entry
end
end
end -- local function setAllToggles
local function iupText(strVal) -- Return multiline text handle
return iup.text { Value=strVal; MultiLine="Yes"; AutoHide="Yes"; Formatting="Yes"; AddFormatTag=iup.user { Bulk="Yes"; CleanOut="Yes"; }; }
end -- local function iupText
local intItem = ( tonumber(TblOption.Set["INDI"]) or 0 ) + ( tonumber(TblOption.Set["FAM"]) or 0 )
if intItem > 9000 then -- Progress Bar for doPopulateCombo() function use of fhFunc -- V2.6
progbar.Setup()
progbar.Start("Initialisation",4) -- Initialisation Progress Bar, 2 x number of fhFunc in tblFilters above -- V2.6
progbar.Message("Finding Database Facts ~ Please Wait")
end
-- Create GUI controls
local lblCommands = iup.label { Title=" This plugin is intended for those scenarios that cannot be handled by these commands: \n 'Edit > Find and Replace...' and 'Tools > Work with Data > Places...' or 'Addresses...' \n It is NOT advisable to use those commands to amend Fact Place field names. "; Alignment="ACENTER"; } -- V3.2 -- V3.5
local lstSetScope = iupList ( tblFilters.SearchScope ) -- V2.3
local btnSomeRecs = iup.button { Title=" Select Records"; Padding="1x1"; Alignment=":ATOP"; } -- V2.8 -- V3.0
local lblSomeRecs = iup.label { Title="All possible records are selected "; } -- V2.8 -- V3.0
local frmSetScope = iup.frame { Title=" Search Scope "; iup.hbox { lstSetScope; btnSomeRecs; lblSomeRecs; Gap="14"; Margin="6x6"; }; } -- V3.0 Gap="19" -- V3.1 Gap="14"
local boxBasicShort = iup.vbox { iup.hbox { Homogeneous="Yes"; }; } doAppend( boxBasicShort, "BasicShort" )
local boxBasicLong = iup.vbox { iup.hbox { Homogeneous="Yes"; }; } doAppend( boxBasicLong , "BasicLong" )
local frmBasicFilt = iup.frame { Title=" Basic Filters "; iup.hbox { boxBasicShort; boxBasicLong; Homogeneous="Yes"; Margin="6x2"; }; }
local lblSeekText = iup.label { Title="Search: "; }
local txtSeekText = iupText ( tblSet.Seek ) txtSeekText.valuechanged_cb=function(self) setToolTip(self,lblSeekText) doChange(self) end txtSeekText.caret_cb=function(self,lin,col,pos) doCaret(self,pos) end -- V3.0
local lblSwapText = iup.label { Title="Replace: "; }
local txtSwapText = iupText ( tblSet.Swap ) txtSwapText.valuechanged_cb=function(self) setToolTip(self,lblSwapText) doChange(self) end txtSwapText.caret_cb=function(self,lin,col,pos) doCaret(self,pos) end -- V3.0
local tglTextPlain = iup.toggle { Title="Plain Text Mode"; }
local tglTextPatt = iup.toggle { Title="LUA Pattern Mode"; }
local radTextMode = iupRadio ( tblTgl.Text, tglTextPlain, tglTextPatt )
local tglCaseInse = iup.toggle { Title="Case Insensitive" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local tglCaseSens = iup.toggle { Title="Case Sensitive" ; action=function(self) setToolTip(self) end; }
local radCaseMode = iupRadio ( tblTgl.Case, tglCaseInse, tglCaseSens )
local tglWordWhole = iup.toggle { Title="Whole Words" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local tglWordPart = iup.toggle { Title="Part Words" ; action=function(self) setToolTip(self) end; }
local radWordMode = iupRadio ( tblTgl.Word, tglWordWhole, tglWordPart )
local tglWhSpHide = iup.toggle { Title="Hide White Space"; } -- V3.0
local tglWhSpShow = iup.toggle { Title="Show White Space"; } -- V3.0
local radWhSpMode = iupRadio ( tblTgl.WhSp, tglWhSpHide, tglWhSpShow )
local tglConfirm = iup.toggle { Title="Confirm the action for every item found"; Value=tblTgl.Conf or "ON"; }
local boxOptLeft = iup.hbox { iup.vbox { lblSeekText; lblSwapText; }; iup.vbox { txtSeekText; txtSwapText; }; Margin="6x0"; }
local boxOptRight = iup.vbox { radTextMode; radCaseMode; radWordMode; radWhSpMode; iup.hbox { tglConfirm; Margin="6x0"; }; Margin="6x2"; } -- V2.8 -- V3.0
local boxOptions = iup.hbox { boxOptLeft; boxOptRight; Homogeneous="Yes"; Margin="0x2"; }
local frmOptions = iup.frame { Title=" Search Criteria "; boxOptions; }
local lblSetPreset = iup.label { Title="Preset: "; } -- V2.5
local lstSetPreset = iup.list { Value=1; DropDown="Yes"; VisibleColumns=7; Visible_Items=20; } -- V2.5
local tglSetPreset = iup.toggle { Title="Lock"; action=function(self) setToolTip(self) end; } -- V2.5
local btnSetPreset = iup.button { Title="Manage Presets"; } -- V2.5
local boxSetPreset = iup.hbox { lblSetPreset; lstSetPreset; tglSetPreset; btnSetPreset; Gap="6"; Margin="6x6"; } -- V2.5
local frmSetPreset = iup.frame { Title=" Manage Presets "; boxSetPreset; } -- V2.5
local btnBasicRepl = iup.button { Title=" Search && Replace"; }
local btnBasicSeek = iup.button { Title=" Search ONLY "; }
local boxBasicBtn = iup.hbox { btnBasicRepl; btnBasicSeek; Homogeneous="Yes"; Gap="6"; Margin="6x6"; } -- V2.5
local frmBasicBtn = iup.frame { Title=" Search & Replace "; boxBasicBtn; } -- V2.5
local btnShortInc = iup.button { Title="Include All Short Text"; }
local btnShortExc = iup.button { Title="Exclude All Short Text"; }
local boxShortText = iup.vbox { iup.hbox { btnShortInc; btnShortExc; Homogeneous="Yes"; }; }
local frmShortText = iup.frame { Title=" Short Text Fields "; boxShortText; } doAppend( boxShortText, "ShortText" )
local btnLongInc = iup.button { Title="Include All Long Text"; }
local btnLongExc = iup.button { Title="Exclude All Long Text"; }
local boxLongText = iup.vbox { iup.hbox { btnLongInc; btnLongExc; Homogeneous="Yes"; }; }
local frmLongText = iup.frame { Title=" Long Text Fields "; boxLongText; } doAppend( boxLongText, "LongText" )
local tglAge_Inc = iup.toggle { Title="Age Warnings" ; action=function(self) setToolTip(self) end; TipBalloonTitle="BEWARE!"; TipBalloonTitleIcon="2"; }
local tglAge_Exc = iup.toggle { Title="No Age Warnings" ; action=function(self) setToolTip(self) end } -- Refresh XP Tooltips
local radAge_Mode = iupRadio ( tblTgl.Age, tglAge_Inc, tglAge_Exc )
local tglDateInc = iup.toggle { Title="Date Warnings" ; action=function(self) setToolTip(self) end; TipBalloonTitle="BEWARE!"; TipBalloonTitleIcon="2"; }
local tglDateExc = iup.toggle { Title="No Date Warnings" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local radDateMode = iupRadio ( tblTgl.Date, tglDateInc, tglDateExc )
local tglPhraseInc = iup.toggle { Title="Date Phrase Warnings" ; action=function(self) setToolTip(self) end; } -- V2.2
local tglPhraseExc = iup.toggle { Title="No Date Phrase Warnings" ; action=function(self) setToolTip(self) end; } -- Refresh XP Tooltips
local radPhraseMode = iupRadio ( tblTgl.Phrase, tglPhraseInc, tglPhraseExc )
local boxDistinct = iup.vbox { }
local frmDistinct = iup.frame { Title=" Distinctive Fields "; boxDistinct; } doAppend( boxDistinct, "Distinct", radAge_Mode, radDateMode, radPhraseMode ) -- V2.2
local btnExtraRepl = iup.button { Title=" Search && Replace"; }
local btnExtraSeek = iup.button { Title=" Search ONLY "; }
local boxExtraBtn = iup.hbox { btnExtraRepl; btnExtraSeek; Homogeneous="Yes"; Gap="6"; Margin="6x6"; }
local frmExtraBtn = iup.frame { Title=" Search & Replace "; boxExtraBtn; }
local btnDefault = iup.button { Title="Restore Defaults"; }
local btnSetFont = iup.button { Title="Set Window Fonts"; }
local btnGetHelp = iup.button { Title=" Help && Advice"; }
local btnDestroy = iup.button { Title="Close Plugin"; action=function() doDestroy() return iup.CLOSE end; }
local boxButtons = iup.hbox { btnDefault; btnSetFont; btnGetHelp; btnDestroy; Homogeneous="Yes"; Gap="5"; Margin="16x0"; }
local boxMajor = iup.vbox { lblCommands; frmSetScope; frmBasicFilt; frmOptions; iup.hbox { frmSetPreset; frmBasicBtn; Homogeneous="Yes"; }; Margin="0"; } -- V2.5 -- V3.0
local boxExtra = iup.hbox { frmShortText; iup.vbox { frmLongText; frmDistinct; frmExtraBtn; }; Homogeneous="Yes"; Margin="0"; } -- V2.5
-- Create the Tab controls layout
local tabControl = iup.tabs {
boxMajor; TabTitle0=" Major Options ";
boxExtra; TabTitle1=" Extra Filters ";
}
local dialogMain = iup.dialog { Title=iup_gui.Plugin..iup_gui.Version; Gap="1"; Margin="2x0"; iup.vbox { tabControl; boxButtons; }; }
local tblConfirm = { ON = "Confirmation prompt for every item found"; OFF = "Process all matching text unconditionally"; }
local tblTextTip = { ON = "Plain text"; OFF = "LUA pattern"; } -- V2.8
local tblActive = { ON = "Yes"; OFF = "No"; } -- V2.8
if fhGetAppVersion() > 6 then -- FH V7 IUP 3.28 -- V3.1
tabControl.TabPadding="8x4"
else -- FH V6 IUP 3.11 -- V3.1
tabControl.Padding="8x4"
end
progbar.Close() -- Close Progress Bar of Initialiation for Finding Record Facts -- V2.6
local function setTabMode(intTab) -- Transfer toggles according to chosen tab called from setControls, saveOptions, tabControl:tabchangepos_cb
intTab = intTab or IntTabPosn -- Default to current tab
if intTab < 2 then
local strTextValue = nil
for strName, anyValue in pairs ( tblExtra ) do -- Transfer the Extra Filters to the Basic Filters
local strExtraValue = tblExtra[strName].Value -- Extra Filter is "ON" or "OFF"
if strExtraValue == "ON"
and tblCombo[strName]
and tblCombo[strName].Value > "1" then -- Extra Filter is "ON" and combo drop list selects subset
strExtraValue = "NOTDEF" -- So Extra Filter is "NOTDEF"
end
if tblBasic[strName] and strName ~= "text" then -- Set each Basic Filter equal to Extra Filter (except "All other Text fields")
tblBasic[strName].Value = strExtraValue
else
strTextValue = strTextValue or strExtraValue -- If other Extra Filters all "ON" or all "OFF" then use that value
if strExtraValue ~= strTextValue then
strTextValue = "NOTDEF" -- Otherwise, with a mixture, use "NOTDEF" value
end
end
end
tblBasic["text"].Value = strTextValue -- Set the Basic "All other Text fields" to this value
elseif intTab == 2 then
for strName, anyValue in pairs ( tblExtra ) do -- Transfer the Basic Filters to the Extra Filters
local strBasicValue = tblBasic["text"].Value -- Default for Extra Filters uses Basic "All other Text fields"
if tblBasic[strName] then
strBasicValue = tblBasic[strName].Value -- But matching Extra Filter uses Basic Filter
end
if strBasicValue ~= "NOTDEF" then -- Unless the Basic Filter is "NOTDEF"
tblExtra[strName].Value = strBasicValue
if tblCombo[strName] and strBasicValue == "ON" then -- Filter is "ON" so reset combo drop list to 1st entry
tblCombo[strName].Value = 1
end
end
end
end
end -- local function setTabMode
local function setTextMode() -- Set Text Case & Word Modes called from setControls, tglTextPlain:action
tglTextPlain.Tip = tglTextPlain.Tip
tglTextPatt.Tip = tglTextPatt.Tip -- Refresh XP Tooltips
radCaseMode.Active = tblActive[tglTextPlain.Value] -- Plain Text Mode enables Case & Word modes -- V2.8
radWordMode.Active = tblActive[tglTextPlain.Value] -- LUA Pattern Mode disables Case & Word modes -- V2.8
lblSeekText.Tip = tblTextTip[tglTextPlain.Value].." string to search for"
txtSeekText.Tip = lblSeekText.Tip
lblSwapText.Tip = tblTextTip[tglTextPlain.Value].." string to substitute"
txtSwapText.Tip = lblSwapText.Tip
end -- local function setTextMode
local function setWhiteSpaceMode() -- Set Seek & Swap text White Space mode on/off -- V3.0
tglWhSpHide.Tip = tglWhSpHide.Tip
tglWhSpShow.Tip = tglWhSpShow.Tip -- Refresh XP Tooltips
tblTgl.WhSp = tglWhSpHide.Value
if tglWhSpHide.Value == "ON" then
txtSeekText.Value = strHideWhiteSpace(txtSeekText.Value)
txtSwapText.Value = strHideWhiteSpace(txtSwapText.Value)
else
txtSeekText.Value = strShowWhiteSpace(txtSeekText.Value)
txtSwapText.Value = strShowWhiteSpace(txtSwapText.Value)
end
end -- local function setWhiteSpaceMode
local function setWarnMode() -- Set Age & Date Warning Mode called from setControls, saveOptions, tblExtra.age:action, tblExtra.date:action, tabControl:tabchangepos_cb
tblExtra.age.Tip = tblExtra.age.Tip
tblExtra.date.Tip = tblExtra.date.Tip -- Refresh XP Tooltips
radAge_Mode.Active = tblActive[tblExtra.age.Value] -- Age Fields included/excluded enables/disables Warning mode -- V2.8
radDateMode.Active = tblActive[tblExtra.date.Value] -- Date Fields included/excluded enables/disables Warning mode -- V2.8
radPhraseMode.Active = tblActive[tblExtra.date.Value]
end -- local function setWarnMode
local function doMediaWarning() -- Warn of using "Multimedia Linked File fields & Media files" -- V4.0
local tblWarning = {
"'Multimedia Linked File fields && Media files'";
"This filter tries to sync each File link field and Windows file path to avoid broken links.";
"So linked Windows file paths may get changed wherever they exist on disc.";
"Beware of the impact on Windows files if they are shared with other products.";
"Changes to Windows files will NOT be reversed by Edit > Undo Plugin Updates.";
"See a full explanation in the Help && Advice page for the Extra Filters tab.";
" "; }
iup_gui.MemoDialogue( table.concat(tblWarning," \n\n ") )
end -- local function doMediaWarning()
local dicRecName = { INDI="Individual (INDI)"; FAM="Family (FAM)"; NOTE="Note (NOTE)"; SOUR="Source (SOUR)"; REPO="Repository (REPO)"; OBJE="Multimedia (OBJE)"; _PLAC="Place (_PLAC)"; _RNOT="Research Note (_RNOT)"; _SRCT="Source Template (_SRCT)"; SUBM="Submitter (SUBM)"; SUBN="Submission (SUBN)"; } -- V3.1
local function setScopeMode() -- Set Search Scope called from setControls, lstSetScope:action, btnSomeRecs:action -- V2.8
local strSize = "All "
local strType = "possible"
local strText = " records are selected "
if tblSet.Recs then
strSize = tblSet.Recs.Size or "All " -- Number of Record Id selected
if strSize == "1 " then strText = " record is selected " end
end
if lstSetScope.Value == "1" then
btnSomeRecs.Active = "No" -- Set Select Records button inactive when all records chosen
else
btnSomeRecs.Active = "Yes" -- Set Select Records button active and identify record type
strType = dicRecName[strScopeType(lstSetScope[lstSetScope.Value]:match(strComboTag()))] or "Error" -- Catch error -- V3.6
end
lblSomeRecs.Title = strSize..strType..strText
end -- local function setScopeMode
local function setControls() -- Reset GUI control values called from lst/tgl/btnSetPreset:action, btnDefault:action
local intCurr = tonumber(tblSet.Curr)
if TblOption[intCurr] then
CopyPreset(TblOption[intCurr],TblOption) -- Load the Preset database into current settings -- V2.5
end
for intList, dicList in ipairs (tblSet.List) do
lstSetPreset[intList] = dicList.Name -- Load the Preset database names into droplist -- V2.5
end
local arrFrame = {frmOptions;frmBasicFilt;frmShortText;frmLongText;frmDistinct;btnDefault;}
lstSetPreset.Value = intCurr
tglSetPreset.Value = tblSet.List[intCurr].Lock or "OFF" -- Enable/disable frames depending on Preset Lock -- V2.5
if tglSetPreset.Value == "OFF" then
for intFrame, iupFrame in ipairs (arrFrame) do iupFrame.Active = "Yes" end
else
for intFrame, iupFrame in ipairs (arrFrame) do iupFrame.Active = "No" end
end
txtSeekText.Value = tblVal.Seek
txtSwapText.Value = tblVal.Swap
txtSeekText.AddFormatTag = iup.user { Bulk="Yes"; CleanOut="Yes"; FgColor=iup_gui.Safe; } -- Special Multiline Formatted Text case!!!
txtSwapText.AddFormatTag = iup.user { Bulk="Yes"; CleanOut="Yes"; FgColor=iup_gui.Safe; }
tglConfirm.Value = tblTgl.Conf
tglConfirm.Tip = tblConfirm[tglConfirm.Value]
radTextMode.Value = iupValue(tblTgl.Text ,tglTextPlain,tglTextPatt)
radCaseMode.Value = iupValue(tblTgl.Case ,tglCaseInse ,tglCaseSens)
radWordMode.Value = iupValue(tblTgl.Word ,tglWordWhole,tglWordPart)
radWhSpMode.Value = iupValue(tblTgl.WhSp ,tglWhSpHide ,tglWhSpShow) -- V3.0
radAge_Mode.Value = iupValue(tblTgl.Age ,tglAge_Inc ,tglAge_Exc )
radDateMode.Value = iupValue(tblTgl.Date ,tglDateInc ,tglDateExc )
radPhraseMode.Value= iupValue(tblTgl.Phrase,tglPhraseInc,tglPhraseExc) -- V2.2
for strName, iupValue in pairs ( tblExtra ) do
tblExtra[strName].Value = tblTgl[strName] or "ON"
end
dialogMain:map() -- Needed to ensure Attributes combo drop list is sorted before setComboValues
setComboValues() -- Set values for matching combo drop list tags -- V2.1
setTabMode() -- Set toggles for current tab, must come after setComboValues()
setTextMode() -- Set Text Case & Word Modes depending on Plain Text/LUA Pattern mode
setWhiteSpaceMode() -- Set Seek & Swap text white space symbols on or off -- V3.0
setWarnMode() -- Set Date & Age Warning mode depending on Date & Age toggle settings
setScopeMode() -- Set Select Records mode and update records selected status -- V2.8
end -- local function setControls
-- Assign GUI control attributes -- V2.1
local tblControls = { { "Font"; "FgColor"; "Expand"; "Tip", {"TipBalloon";"Balloon"}; { "help_cb";function() iup_gui.HelpDialogue(IntTabPosn) end }; "action"; setControls; };
[tabControl ] = { "FontHead"; "Head"; "Yes" ; "Select 'Major Options' or 'Extra Filters'"; };
[frmSetScope] = { "FontHead"; "Head"; "Horizontal"; }; -- V3.0
[boxBasicShort] = { "FontBody"; "Safe"; "Yes"; };
[boxBasicLong] = { "FontBody"; "Safe"; "Yes"; };
[frmBasicFilt] = { "FontHead"; "Head"; "Yes"; };
[boxOptions ] = { "FontBody"; "Safe"; "Yes"; };
[frmOptions ] = { "FontHead"; "Head"; "Yes"; };
[boxSetPreset] = { "FontBody"; "Safe"; "Yes"; }; -- V2.5
[frmSetPreset] = { "FontHead"; "Head"; "Horizontal"; }; -- V2.5
[boxBasicBtn] = { "FontBody"; "Safe"; "Yes"; }; -- V2.5
[frmBasicBtn] = { "FontHead"; "Head"; "Horizontal"; }; -- V2.5
[boxShortText] = { "FontBody"; "Safe"; "Yes"; };
[frmShortText] = { "FontHead"; "Head"; "Yes"; };
[boxLongText] = { "FontBody"; "Safe"; "Yes"; };
[frmLongText] = { "FontHead"; "Head"; "Yes"; };
[boxDistinct] = { "FontBody"; "Safe"; "Yes"; };
[frmDistinct] = { "FontHead"; "Head"; "Yes"; };
[boxExtraBtn] = { "FontBody"; "Safe"; "Yes"; }; -- V2.5
[frmExtraBtn] = { "FontHead"; "Head"; "Horizontal"; }; -- V2.5
[lblCommands] = { "FontHead"; "Risk"; "Yes" ; "The standard commands may offer a simpler solution than this plugin "; }; -- V3.2
[lstSetScope] = { "FontBody"; "Safe"; "Yes" ; "Set global search scope for Basic/Extra Filters"; }; -- V3.0
[btnSomeRecs] = { "FontBody"; "Safe"; "Horizontal"; "Select records to include in search\nwhere type matches Search Scope"; }; -- V2.8
[lblSomeRecs] = { "FontBody"; "Body"; "Yes" ; "Selected records to include in Search Scope"; }; -- V2.8
[lblSeekText] = { "FontBody"; "Body"; "Vertical" ; "Plain text string to search for"; };
[txtSeekText] = { "FontBody"; "Safe"; "Yes" ; "Plain text string to search for"; };
[lblSwapText] = { "FontBody"; "Body"; "Vertical" ; "Plain text string to substitute"; };
[txtSwapText] = { "FontBody"; "Safe"; "Yes" ; "Plain text string to substitute"; };
[tglTextPlain] = { "FontBody"; "Safe"; "Yes" ; "Plain text strings"; };
[tglTextPatt] = { "FontBody"; "Safe"; "Yes" ; "LUA pattern strings\nSee 'Help and Advice' for details"; };
[tglCaseInse] = { "FontBody"; "Safe"; "Yes" ; "Case insensitive text search"; };
[tglCaseSens] = { "FontBody"; "Safe"; "Yes" ; "Case sensitive text search"; };
[tglWordWhole] = { "FontBody"; "Safe"; "Yes" ; "Whole words are delimited by either\nspace, tab, newline, punctuation, or field edge"; };
[tglWordPart] = { "FontBody"; "Safe"; "Yes" ; "Part words may be any text"; };
[tglWhSpHide] = { "FontBody"; "Safe"; "Yes" ; "Hide white space symbols"; }; -- V3.0
[tglWhSpShow] = { "FontBody"; "Safe"; "Yes" ; "Show white space symbols"; }; -- V3.0
[tglConfirm ] = { "FontBody"; "Safe"; "Yes" ; tblConfirm[tglConfirm.Value] ; function() tglConfirm.Tip = tblConfirm[tglConfirm.Value] end; };
[lblSetPreset] = { "FontBody"; "Body"; "Vertical" ; "Choose the current Preset settings"; }; -- V2.5
[lstSetPreset] = { "FontBody"; "Safe"; "Yes" ; "Choose the current Preset settings"; }; -- V2.5
[tglSetPreset] = { "FontBody"; "Safe"; "Vertical" ; "Lock the Preset to prevent changes"; }; -- V2.5
[btnSetPreset] = { "FontBody"; "Safe"; "Yes" ; "Manage the Preset Settings database"; }; -- V2.5
[btnBasicRepl] = { "FontHead"; "Safe"; "Yes" ; "Start the Search and Replace process"; };
[btnBasicSeek] = { "FontHead"; "Safe"; "Yes" ; "Start the Search only process"; };
[btnShortInc] = { "FontBody"; "Safe"; "Yes" ; "Include all one line Short Text fields" ; function() setAllToggles("ShortText","ON") doMediaWarning() end; }; -- V4.0
[btnShortExc] = { "FontBody"; "Risk"; "Yes" ; "Exclude all one line Short Text fields" ; function() setAllToggles("ShortText","OFF") end; };
[btnLongInc ] = { "FontBody"; "Safe"; "Yes" ; "Include all multi-line Long Text fields" ; function() setAllToggles("LongText","ON") end; };
[btnLongExc ] = { "FontBody"; "Risk"; "Yes" ; "Exclude all multi-line Long Text fields" ; function() setAllToggles("LongText","OFF") end; };
[tglAge_Inc ] = { "FontBody"; "Risk"; "Yes" ; "BEWARE: Affects record 'Updated' date\neven if the Age fields are skipped ! !"; };
[tglAge_Exc ] = { "FontBody"; "Safe"; "Yes" ; "Age Warnings disabled"; };
[tglDateInc ] = { "FontBody"; "Risk"; "Yes" ; "BEWARE: Affects record 'Updated' date\neven if the Date fields are skipped ! !"; };
[tglDateExc ] = { "FontBody"; "Safe"; "Yes" ; "Date Warnings disabled"; };
[tglPhraseInc] = { "FontBody"; "Risk"; "Yes" ; "Date Phrase Warnings enabled"; }; -- V2.2
[tglPhraseExc] = { "FontBody"; "Safe"; "Yes" ; "Date Phrase Warnings disabled"; }; -- V2.2
[btnExtraRepl] = { "FontHead"; "Safe"; "Yes" ; "Start the Search and Replace process"; };
[btnExtraSeek] = { "FontHead"; "Safe"; "Yes" ; "Start the Search only process"; };
[btnDefault ] = { "FontBody"; "Safe"; "Horizontal"; "Restore default Settings for Options and Window positions and sizes"; };
[btnSetFont ] = { "FontBody"; "Safe"; "Horizontal"; "Choose user interface window font style"; };
[btnGetHelp ] = { "FontBody"; "Safe"; "Horizontal"; "Obtain online Help and Advice from the Plugin Store"; };
[btnDestroy ] = { "FontBody"; "Risk"; "Horizontal"; "Close the Plugin"; };
}
iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes -- V2.1
local function saveOptions() -- Save all GUI settings called from doSearchReplace, doSearchOnly, lst/tgl/btnSetPreset:action, btnSetFont:action, btnDestroy:action
if IntTabPosn < 2 then
setTabMode(2) -- Transfer Basic Filters to Extra Filters if necessary
setWarnMode()
end
for strName, anyValue in pairs ( tblTgl ) do
if not tblExtra[strName] then
tblTgl[strName] = nil -- Clear the non-GUI Class and Tag toggles
end
end
for strName, anyValue in pairs ( tblExtra ) do
tblTgl[strName] = tblExtra[strName].Value -- Save all the GUI Class and Tag and "Attr" toggle filters
end
for strName, anyValue in pairs ( tblCombo ) do
local strText = tblCombo[strName][tblCombo[strName].Value]
tblLst[strName] = strText:match(strComboTag()) or "" -- Save all the GUI combo drop list tag filters
end
tblVal.Seek = strHideWhiteSpace(txtSeekText.Value) -- Save text strings -- V2.5 -- Hide white space symbols -- V3.0
tblVal.Swap = strHideWhiteSpace(txtSwapText.Value)
tblSet.Mode = nil -- Mode and Data Ref are not saved
tblSet.Data = nil
tblTgl.Conf = tglConfirm.Value -- Save all mode toggles
tblTgl.Text = tglTextPlain.Value
tblTgl.Case = tglCaseInse.Value
tblTgl.Word = tglWordWhole.Value
tblTgl.WhSp = tglWhSpHide.Value -- V3.0
tblTgl.Age = tglAge_Inc.Value
tblTgl.Date = tglDateInc.Value
tblTgl.Phrase= tglPhraseInc.Value -- V2.2
local intCurr = tonumber(tblSet.Curr)
tblSet.List[intCurr].Lock = tglSetPreset.Value
if not TblOption[intCurr] then TblOption[intCurr] = {} end -- V2.5
CopyPreset(TblOption,TblOption[intCurr]) -- Save the Preset database from current settings -- V2.5
SaveSettings() -- Save sticky data settings
end -- local function saveOptions
function tglTextPlain:action(intState) -- Action for Plain Text Mode v LUA Pattern Mode radio toggle
setTextMode()
end -- function tglTextPlain:action
function tglWhSpHide:action(intState) -- Action for Hide/Show White Space symbols radio toggle -- V3.0
setWhiteSpaceMode()
end -- function tglWhSpHide:action
function lstSetScope:action(strText,intItem,intState) -- Action for Search Scope drop list -- V2.8
lstSetScope.Tip = lstSetScope.Tip -- Refresh XP Tooltip
if intState == 1 then
tblSet.Recs = nil -- Enable all Record Id
setScopeMode() -- Make the Select Records button active depending on Search Scope droplist
end
end -- lstSetScope:action
function btnSomeRecs:action() -- Action for Select Records button -- V2.8
dialogMain.Active="No"
local strType = strScopeType(lstSetScope[lstSetScope.Value]:match(strComboTag())) or "INDI"
local tblRecs = fhPromptUserForRecordSel(strType) -- Select Record Id for Search Scope record type tag
tblSet.Recs = nil -- Enable all Record Id
if #tblRecs > 0 then
tblSet.Recs = {}
tblSet.Recs.Size = tostring(#tblRecs).." " -- Save number of Record Id selected
for intRec, ptrRec in ipairs (tblRecs) do
tblSet.Recs[fhGetRecordId(ptrRec)] = true -- Tabulate each Record Id selected
end
end
setScopeMode() -- Update status message of records selected
dialogMain.Active="Yes"
dialogMain.BringFront="Yes"
end -- function btnSomeRecs:action
function tblExtra.age:action(intState) -- Action for Age fields toggle
setWarnMode()
end -- function tblExtra.age:action
function tblExtra.date:action(intState) -- Action for Date fields toggle
setWarnMode()
end -- function tblExtra.date:action
function tblExtra.FILE:action(intState) -- Action for Multimedia Linked File fields & Media files toggle -- V4.0
if intState == 1 then
doMediaWarning()
end
end -- function tblExtra.FILE:action
local function doPerformAction() -- Perform action for both Search & Replace and Search ONLY button
if tblVal.Seek:match("\n") or tblVal.Swap:match("\n") then -- Check newline characters are only used with Long Text fields -- V2.1
local strToggle = "OFF"
for intItem, tblItem in ipairs ( tblFilters.ShortText ) do
if tblTgl[tblItem.Name] == "ON" then -- Must check before true/false adjustment below as main toggle may get disabled -- V2.4
strToggle = "ON" -- Short Text option selected
break
end
end
if strToggle == "ON" -- Short Text or Record Title or Age or Date selected?
or tblTgl["SOUR.TITL"] == "ON" -- Source Title is Long Text, but others are Short Text, and Source Title unlikely to have newlines
or tblTgl.age == "ON"
or tblTgl.date == "ON" then
iup_gui.MemoDialogue("\n Newline characters in Search &&/or Replace text \n are reserved exclusively for Long Text Fields, \n such as Note, Fact Address, Text from Source. \n")
return false
end
end
for strName, strValue in pairs ( tblTgl ) do -- TblOptions can be altered as SaveSettings() is not now called except via saveOptions()
tblTgl[strName] = ( strValue == "ON" ) -- Swap "ON" to true, and "OFF" to false, in all toggle options
end
for strSource, tblTarget in pairs ( tblRelated ) do
for _, strTarget in ipairs ( tblTarget ) do -- Settings for related target Classes & Tags from source options
tblTgl[strTarget] = tblTgl[strSource]
end
end
for strName, anyValue in pairs ( tblCombo ) do -- Adjust true/false settings for related combo drop list tags
if tblTgl[strName] and tblLst[strName] ~= "" then -- Matching extra toggle option is ticked and combo drop list names a tag
local strTag = tblLst[strName]
strTag = strTag:replace(".NOTE",".NOTE2")
strTag = strTag:replace("~OBJE","~OBJE2")
tblTgl[strName] = ( strName == strTag )
for _, strTarget in ipairs ( tblRelated[strName] or {} ) do
tblTgl[strTarget] = ( strTarget == strTag )
end
tblTgl[strTag] = true
end
end
if tblTgl.Text then -- Plain Text mode -- 1 Jan 2014
tblTgl.Text = " Plain Text "
if tblVal.Seek:match("^["..StrSP.."]+$") then -- V2.1 Exclude seek for white space &/or punctuation from Whole Word search
tblTgl.Word = false
end
if tblTgl.Word -- Whole Word mode -- 1 Jan 2014
and tblVal.Seek ~= "" then -- Except empty search string -- 5 Jul 2014 V2.3
local strDelimit = "(["..StrSP.."\02])" -- Replace whole words delimited by white space or punctuation or STX="\02" characters
tblVal.Seek = tblVal.Seek:gsub("(["..StrSP.."])","%1\02"):plain()
tblVal.Swap = "%1"..tblVal.Swap:inert().."%2"
tblVal.Seek = strDelimit..tblVal.Seek..strDelimit -- Add word delimiters
else
tblVal.Seek = tblVal.Seek:plain() -- Hide any LUA pattern magic characters
tblVal.Swap = tblVal.Swap:inert()
end
if tblTgl.Case then -- Case Insensitive mode
tblVal.Seek = tblVal.Seek:caseless()
end
else
tblTgl.Text = " LUA Pattern " -- LUA Pattern search mode
tblTgl.Case = false -- Must disable Case Insensitive and Whole Word searches
tblTgl.Word = false
end
if tblVal.Seek == "" then
tblVal.Seek = "^$" -- Empty search string must use empty LUA pattern -- V2.0
tblTgl.Word = false -- and disable Whole Word search mode
end
if tblVal.Swap == "" then -- Change to any char < "\07" as it cannot be enterd by user even via Alt+001
tblVal.Swap = "\01" -- Use SOH="\01" character so DoSeekMatch() always detects a change -- V2.0
end
local tblUser = iup_gui.DialogueAttributes("Main") -- Customise the Replace dialogue to use Main dialogue size & position
iup_gui.CustomDialogue("User",tblUser.Raster,tblUser.CoordX,tblUser.CoordY)
DoAllItems() -- Perform the Search and optional Replace process
return true
end -- local function doPerformAction
local function doOutputResultSet(strTitle,strSub,strOldVal,strNewVal) -- Output the Result Set details -- V3.1
if #TblRecord == 0 then -- No matching data, so report and clear Result Set
local strMode = "reported"
if strNewVal then strMode = "replaced" end
iup_gui.MemoDialogue("\n No matching data items have been "..strMode..". \n")
TblResultSet = { Title=strTitle; Sub=strSub; Old=strOldVal; New=strNewVal; }
return false
end
TblResultSet = {}
fhOutputResultSetTitles(strTitle..strSub, strTitle, strSub.." Date: %#x")
fhOutputResultSetColumn("Record Name", "item", TblRecord , #TblRecord, 180, "align_left")
fhOutputResultSetColumn("Data Ref" , "text", TblDataRef , #TblRecord, 150, "align_left")
fhOutputResultSetColumn("Data Ref" , "item", TblRefValue, #TblRecord, 250, "align_left", 0, true, "default", "buddy")
fhOutputResultSetColumn( strOldVal , "text", TblOldValue, #TblRecord, 250, "align_left")
if strNewVal then
fhOutputResultSetColumn( strNewVal , "text", TblNewValue, #TblRecord, 250, "align_left") -- Only for replacement
end
fhOutputResultSetColumn("Ref Value" , "item", TblRefValue, #TblRecord, 250, "align_left", 0, true, "default", "buddy")
return true
end -- local function doOutputResultSet
local function doSearchReplace() -- Search & Replace action
saveOptions()
local strSeek = tblVal.Seek
local strSwap = tblVal.Swap
tblSet.Mode = "Replace"
if doPerformAction() then
if not TblOption.Tgl.WhSp then -- V3.1
strSeek = strShowWhiteSpace(strSeek)
strSwap = strShowWhiteSpace(strSwap)
end
local strTitle = "Search and Replace Results " -- Output Result Set for replacements -- V3.1
local strSubTitle = "Replacing"..tblTgl.Text..strSeek.." with "..strSwap
if doOutputResultSet(strTitle,strSubTitle,"Old Value","New Value") then return iup.CLOSE end
end
end -- local function doSearchReplace
function btnBasicRepl:action() -- Action for Basic Search & Replace button -- V2.1
return doSearchReplace()
end -- function btnBasicRepl:action
function btnExtraRepl:action() -- Action for Extra Search & Replace button
return doSearchReplace()
end -- function btnExtraRepl:action
local function doSearchOnly() -- Search Only action
saveOptions()
local strSeek = tblVal.Seek
tblVal.Swap = "\03" -- Use ETX="\03" character so DoSeekMatch() always detects a change -- 1 Jan 2014
tblSet.Mode = "Report"
if doPerformAction() then
if not TblOption.Tgl.WhSp then -- V3.1
strSeek = strShowWhiteSpace(strSeek)
end
local strTitle = "Search Results " -- Output Result Set for reported text -- V3.1
local strSubTitle = "Searching for"..tblTgl.Text..strSeek
if doOutputResultSet(strTitle,strSubTitle,"Data Value") then return iup.CLOSE end
end
end -- local function doSearchOnly
function btnBasicSeek:action() -- Action for Basic Search ONLY button -- V2.1
return doSearchOnly()
end -- function btnBasicSeek:action
function btnExtraSeek:action() -- Action for Extra Search ONLY button
return doSearchOnly()
end -- function btnExtraSeek:action
function lstSetPreset:action(strCurr,intCurr,intState) -- Action for Set Preset droplist -- V2.5
tblSet.Curr = intCurr
tglSetPreset.Value = tblSet.List[intCurr].Lock or "OFF"
if intState == 0 then
saveOptions() -- Save current settings in old Preset database -- V2.5
else
if not TblOption[intCurr].Val then
UserDefaultOptions(TblOption[intCurr]) -- Create default database for undefined Preset -- V2.5
end
setControls() -- Load new Preset database into current settings -- V2.5
end
setToolTip(lstSetPreset) -- Refresh XP Tooltip -- V2.3
end -- function lstSetPreset
function tglSetPreset:action(intState) -- Action for Lock preset toggle -- V2.5
saveOptions()
setControls()
setToolTip(tglSetPreset) -- Refresh XP Tooltip -- V2.3
end -- function tglSetPreset:action
function btnSetPreset:action() -- Action for Manage Presets button -- V2.5
saveOptions()
ManagePresets()
lstSetPreset[#tblSet.List+1] = iup.NULL -- Curtail shortened Preset dropdown list
setControls() -- Load new Preset database into current settings -- V2.5
end -- function btnSetPreset:action
function btnDefault:action() -- Action for Restore Defaults button
ResetDefaultSettings()
setControls() -- Reset controls & redisplay Main dialogue
iup_gui.ShowDialogue("Main")
SaveSettings() -- Save sticky data settings
end -- function btnDefault:action
function btnSetFont:action() -- Action for Set Window Font button -- V2.1
btnSetFont.Active = "No"
saveOptions()
iup_gui.FontDialogue(tblControls)
SaveSettings() -- Save sticky data settings
btnSetFont.Active = "Yes"
end -- function btnFontSet: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/search-and-replace"
local arrHelp = { "-major-options-tab"; "-extra-filters-tab"; }
function btnGetHelp:action() -- Action for Help & Advice button according to current tab
local strPage = arrHelp[IntTabPosn] or ""
doExecute( strHelp..strPage )
fhSleep(3000,500)
dialogMain.BringFront="YES"
end -- function btnGetHelp:action
function doDestroy() -- Close Plugin actions -- V3.1
saveOptions()
if TblResultSet.Title then -- Clear the Result Set
TblRecord[1] = fhNewItemPtr()
TblDataRef[1] = ""
TblOldValue[1] = "No matching data items"
TblNewValue[1] = ""
TblRefValue[1] = fhNewItemPtr()
doOutputResultSet(TblResultSet.Title,TblResultSet.Sub,TblResultSet.Old,TblResultSet.New)
end
end -- function doDestroy
--[=[
function btnDestroy:action() -- Action for Close Plugin button -- V3.1
doDestroy()
return iup.CLOSE
end -- function btnDestroy:action
--]=]
function dialogMain:close_cb() -- Close X button callback action -- V3.1
doDestroy()
end -- function dialogMain:close_cb
function tabControl:tabchangepos_cb(intNew,intOld) -- Call back when Main tab position is changed
IntTabPosn = intNew + 1
setTabMode() -- Set toggles for current tab
setWarnMode()
-- saveOptions() -- Could save IntTabPosn sticky setting ?
end -- function tabControl:tabchangepos_cb
iup_gui.ShowDialogue("Main",dialogMain,btnDestroy)
end -- function GUI_MainDialogue
-- Main Code Section Starts Here --
fhInitialise(5,0,8,"save_recommended") -- 5.0.8 for Project/User/Machine Plugin Data needed for Presets
PresetGlobalData() -- Preset global data definitions
ResetDefaultSettings() -- Preset default sticky settings
LoadSettings() -- Load sticky data settings
iup_gui.CheckVersionInStore() -- Notify if later Version
GUI_MainDialogue() -- Invoke graphical user interfaceSource:Search-and-Replace-13.fh_lua